DhanHQ 2.1.5 → 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/CHANGELOG.md +7 -0
- data/GUIDE.md +215 -73
- data/README.md +416 -132
- data/README1.md +267 -26
- data/docs/live_order_updates.md +319 -0
- data/docs/rails_websocket_integration.md +847 -0
- data/docs/standalone_ruby_websocket_integration.md +1588 -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/configuration.rb +16 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
- 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/order_update.rb +235 -0
- data/lib/DhanHQ/models/trade.rb +118 -31
- 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/connection.rb +2 -2
- 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 +175 -11
- data/lib/DhanHQ/ws/orders/connection.rb +40 -81
- data/lib/DhanHQ/ws/orders.rb +28 -0
- data/lib/DhanHQ/ws/segments.rb +18 -2
- data/lib/DhanHQ/ws.rb +3 -2
- data/lib/dhan_hq.rb +5 -0
- metadata +35 -1
@@ -0,0 +1,331 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Represents expired options data for rolling contracts
|
7
|
+
# Provides access to OHLC, volume, open interest, implied volatility, and spot data
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
class ExpiredOptionsData < BaseModel
|
10
|
+
# All expired options data attributes
|
11
|
+
attributes :exchange_segment, :interval, :security_id, :instrument,
|
12
|
+
:expiry_flag, :expiry_code, :strike, :drv_option_type,
|
13
|
+
:required_data, :from_date, :to_date, :data
|
14
|
+
|
15
|
+
class << self
|
16
|
+
##
|
17
|
+
# Fetch expired options data for rolling contracts
|
18
|
+
# POST /charts/rollingoption
|
19
|
+
#
|
20
|
+
# @param params [Hash] Parameters for the request
|
21
|
+
# @option params [String] :exchange_segment Exchange segment (e.g., "NSE_FNO")
|
22
|
+
# @option params [Integer] :interval Minute interval (1, 5, 15, 25, 60)
|
23
|
+
# @option params [String] :security_id Security ID for the underlying
|
24
|
+
# @option params [String] :instrument Instrument type ("OPTIDX" or "OPTSTK")
|
25
|
+
# @option params [String] :expiry_flag Expiry interval ("WEEK" or "MONTH")
|
26
|
+
# @option params [Integer] :expiry_code Expiry code
|
27
|
+
# @option params [String] :strike Strike price ("ATM", "ATM+1", "ATM-1", etc.)
|
28
|
+
# @option params [String] :drv_option_type Option type ("CALL" or "PUT")
|
29
|
+
# @option params [Array<String>] :required_data Required data fields
|
30
|
+
# @option params [String] :from_date Start date (YYYY-MM-DD)
|
31
|
+
# @option params [String] :to_date End date (YYYY-MM-DD)
|
32
|
+
# @return [ExpiredOptionsData] Expired options data object
|
33
|
+
def fetch(params)
|
34
|
+
validate_params(params)
|
35
|
+
|
36
|
+
response = expired_options_resource.fetch(params)
|
37
|
+
new(response.merge(params), skip_validation: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def expired_options_resource
|
43
|
+
@expired_options_resource ||= DhanHQ::Resources::ExpiredOptionsData.new
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_params(params)
|
47
|
+
contract = DhanHQ::Contracts::ExpiredOptionsDataContract.new
|
48
|
+
validation_result = contract.call(params)
|
49
|
+
|
50
|
+
return if validation_result.success?
|
51
|
+
|
52
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# ExpiredOptionsData objects are read-only, so no validation contract needed
|
58
|
+
def validation_contract
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Get call option data
|
64
|
+
# @return [Hash, nil] Call option data or nil if not available
|
65
|
+
def call_data
|
66
|
+
return nil unless data.is_a?(Hash)
|
67
|
+
|
68
|
+
data["ce"] || data[:ce]
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Get put option data
|
73
|
+
# @return [Hash, nil] Put option data or nil if not available
|
74
|
+
def put_data
|
75
|
+
return nil unless data.is_a?(Hash)
|
76
|
+
|
77
|
+
data["pe"] || data[:pe]
|
78
|
+
end
|
79
|
+
|
80
|
+
##
|
81
|
+
# Get data for the specified option type
|
82
|
+
# @param option_type [String] "CALL" or "PUT"
|
83
|
+
# @return [Hash, nil] Option data or nil if not available
|
84
|
+
def data_for_type(option_type)
|
85
|
+
case option_type.upcase
|
86
|
+
when "CALL"
|
87
|
+
call_data
|
88
|
+
when "PUT"
|
89
|
+
put_data
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Get OHLC data for the specified option type
|
95
|
+
# @param option_type [String] "CALL" or "PUT"
|
96
|
+
# @return [Hash] OHLC data with open, high, low, close arrays
|
97
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
98
|
+
def ohlc_data(option_type = nil)
|
99
|
+
option_type ||= drv_option_type
|
100
|
+
option_data = data_for_type(option_type)
|
101
|
+
return {} unless option_data
|
102
|
+
|
103
|
+
{
|
104
|
+
open: option_data["open"] || option_data[:open] || [],
|
105
|
+
high: option_data["high"] || option_data[:high] || [],
|
106
|
+
low: option_data["low"] || option_data[:low] || [],
|
107
|
+
close: option_data["close"] || option_data[:close] || []
|
108
|
+
}
|
109
|
+
end
|
110
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
111
|
+
|
112
|
+
##
|
113
|
+
# Get volume data for the specified option type
|
114
|
+
# @param option_type [String] "CALL" or "PUT"
|
115
|
+
# @return [Array<Integer>] Volume data array
|
116
|
+
def volume_data(option_type = nil)
|
117
|
+
option_type ||= drv_option_type
|
118
|
+
option_data = data_for_type(option_type)
|
119
|
+
return [] unless option_data
|
120
|
+
|
121
|
+
option_data["volume"] || option_data[:volume] || []
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Get open interest data for the specified option type
|
126
|
+
# @param option_type [String] "CALL" or "PUT"
|
127
|
+
# @return [Array<Float>] Open interest data array
|
128
|
+
def open_interest_data(option_type = nil)
|
129
|
+
option_type ||= drv_option_type
|
130
|
+
option_data = data_for_type(option_type)
|
131
|
+
return [] unless option_data
|
132
|
+
|
133
|
+
option_data["oi"] || option_data[:oi] || []
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Get implied volatility data for the specified option type
|
138
|
+
# @param option_type [String] "CALL" or "PUT"
|
139
|
+
# @return [Array<Float>] Implied volatility data array
|
140
|
+
def implied_volatility_data(option_type = nil)
|
141
|
+
option_type ||= drv_option_type
|
142
|
+
option_data = data_for_type(option_type)
|
143
|
+
return [] unless option_data
|
144
|
+
|
145
|
+
option_data["iv"] || option_data[:iv] || []
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Get strike price data for the specified option type
|
150
|
+
# @param option_type [String] "CALL" or "PUT"
|
151
|
+
# @return [Array<Float>] Strike price data array
|
152
|
+
def strike_data(option_type = nil)
|
153
|
+
option_type ||= drv_option_type
|
154
|
+
option_data = data_for_type(option_type)
|
155
|
+
return [] unless option_data
|
156
|
+
|
157
|
+
option_data["strike"] || option_data[:strike] || []
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Get spot price data for the specified option type
|
162
|
+
# @param option_type [String] "CALL" or "PUT"
|
163
|
+
# @return [Array<Float>] Spot price data array
|
164
|
+
def spot_data(option_type = nil)
|
165
|
+
option_type ||= drv_option_type
|
166
|
+
option_data = data_for_type(option_type)
|
167
|
+
return [] unless option_data
|
168
|
+
|
169
|
+
option_data["spot"] || option_data[:spot] || []
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Get timestamp data for the specified option type
|
174
|
+
# @param option_type [String] "CALL" or "PUT"
|
175
|
+
# @return [Array<Integer>] Timestamp data array (epoch)
|
176
|
+
def timestamp_data(option_type = nil)
|
177
|
+
option_type ||= drv_option_type
|
178
|
+
option_data = data_for_type(option_type)
|
179
|
+
return [] unless option_data
|
180
|
+
|
181
|
+
option_data["timestamp"] || option_data[:timestamp] || []
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Get data points count for the specified option type
|
186
|
+
# @param option_type [String] "CALL" or "PUT"
|
187
|
+
# @return [Integer] Number of data points
|
188
|
+
def data_points_count(option_type = nil)
|
189
|
+
timestamps = timestamp_data(option_type)
|
190
|
+
timestamps.size
|
191
|
+
end
|
192
|
+
|
193
|
+
##
|
194
|
+
# Get average volume for the specified option type
|
195
|
+
# @param option_type [String] "CALL" or "PUT"
|
196
|
+
# @return [Float] Average volume
|
197
|
+
def average_volume(option_type = nil)
|
198
|
+
volumes = volume_data(option_type)
|
199
|
+
return 0.0 if volumes.empty?
|
200
|
+
|
201
|
+
volumes.sum.to_f / volumes.size
|
202
|
+
end
|
203
|
+
|
204
|
+
##
|
205
|
+
# Get average open interest for the specified option type
|
206
|
+
# @param option_type [String] "CALL" or "PUT"
|
207
|
+
# @return [Float] Average open interest
|
208
|
+
def average_open_interest(option_type = nil)
|
209
|
+
oi_data = open_interest_data(option_type)
|
210
|
+
return 0.0 if oi_data.empty?
|
211
|
+
|
212
|
+
oi_data.sum.to_f / oi_data.size
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# Get average implied volatility for the specified option type
|
217
|
+
# @param option_type [String] "CALL" or "PUT"
|
218
|
+
# @return [Float] Average implied volatility
|
219
|
+
def average_implied_volatility(option_type = nil)
|
220
|
+
iv_data = implied_volatility_data(option_type)
|
221
|
+
return 0.0 if iv_data.empty?
|
222
|
+
|
223
|
+
iv_data.sum.to_f / iv_data.size
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Get price range (high - low) for the specified option type
|
228
|
+
# @param option_type [String] "CALL" or "PUT"
|
229
|
+
# @return [Array<Float>] Price range for each data point
|
230
|
+
def price_ranges(option_type = nil)
|
231
|
+
ohlc = ohlc_data(option_type)
|
232
|
+
highs = ohlc[:high]
|
233
|
+
lows = ohlc[:low]
|
234
|
+
|
235
|
+
return [] if highs.empty? || lows.empty?
|
236
|
+
|
237
|
+
highs.zip(lows).map { |high, low| high - low }
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Get summary statistics for the specified option type
|
242
|
+
# @param option_type [String] "CALL" or "PUT"
|
243
|
+
# @return [Hash] Summary statistics
|
244
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
245
|
+
def summary_stats(option_type = nil)
|
246
|
+
option_type ||= drv_option_type
|
247
|
+
ohlc = ohlc_data(option_type)
|
248
|
+
volumes = volume_data(option_type)
|
249
|
+
oi_data = open_interest_data(option_type)
|
250
|
+
iv_data = implied_volatility_data(option_type)
|
251
|
+
|
252
|
+
{
|
253
|
+
data_points: data_points_count(option_type),
|
254
|
+
avg_volume: average_volume(option_type),
|
255
|
+
avg_open_interest: average_open_interest(option_type),
|
256
|
+
avg_implied_volatility: average_implied_volatility(option_type),
|
257
|
+
price_ranges: price_ranges(option_type),
|
258
|
+
has_ohlc: !ohlc[:open].empty?,
|
259
|
+
has_volume: !volumes.empty?,
|
260
|
+
has_open_interest: !oi_data.empty?,
|
261
|
+
has_implied_volatility: !iv_data.empty?
|
262
|
+
}
|
263
|
+
end
|
264
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
265
|
+
|
266
|
+
##
|
267
|
+
# Check if this is index options data
|
268
|
+
# @return [Boolean] true if instrument is OPTIDX
|
269
|
+
def index_options?
|
270
|
+
instrument == "OPTIDX"
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# Check if this is stock options data
|
275
|
+
# @return [Boolean] true if instrument is OPTSTK
|
276
|
+
def stock_options?
|
277
|
+
instrument == "OPTSTK"
|
278
|
+
end
|
279
|
+
|
280
|
+
##
|
281
|
+
# Check if this is weekly expiry
|
282
|
+
# @return [Boolean] true if expiry_flag is WEEK
|
283
|
+
def weekly_expiry?
|
284
|
+
expiry_flag == "WEEK"
|
285
|
+
end
|
286
|
+
|
287
|
+
##
|
288
|
+
# Check if this is monthly expiry
|
289
|
+
# @return [Boolean] true if expiry_flag is MONTH
|
290
|
+
def monthly_expiry?
|
291
|
+
expiry_flag == "MONTH"
|
292
|
+
end
|
293
|
+
|
294
|
+
##
|
295
|
+
# Check if this is call option data
|
296
|
+
# @return [Boolean] true if drv_option_type is CALL
|
297
|
+
def call_option?
|
298
|
+
drv_option_type == "CALL"
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Check if this is put option data
|
303
|
+
# @return [Boolean] true if drv_option_type is PUT
|
304
|
+
def put_option?
|
305
|
+
drv_option_type == "PUT"
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Check if strike is at the money
|
310
|
+
# @return [Boolean] true if strike is ATM
|
311
|
+
def at_the_money?
|
312
|
+
strike == "ATM"
|
313
|
+
end
|
314
|
+
|
315
|
+
##
|
316
|
+
# Get strike offset from ATM
|
317
|
+
# @return [Integer] Strike offset (0 for ATM, positive for ATM+X, negative for ATM-X)
|
318
|
+
def strike_offset
|
319
|
+
return 0 if at_the_money?
|
320
|
+
|
321
|
+
match = strike.match(/\AATM(\+|-)?(\d+)\z/)
|
322
|
+
return 0 unless match
|
323
|
+
|
324
|
+
sign = match[1] == "-" ? -1 : 1
|
325
|
+
offset = match[2].to_i
|
326
|
+
sign * offset
|
327
|
+
end
|
328
|
+
end
|
329
|
+
# rubocop:enable Metrics/ClassLength
|
330
|
+
end
|
331
|
+
end
|
@@ -7,7 +7,10 @@ module DhanHQ
|
|
7
7
|
# Model wrapper for fetching instruments by exchange segment.
|
8
8
|
class Instrument < BaseModel
|
9
9
|
attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument, :series,
|
10
|
-
:lot_size, :tick_size, :expiry_date, :strike_price, :option_type
|
10
|
+
:lot_size, :tick_size, :expiry_date, :strike_price, :option_type, :underlying_symbol,
|
11
|
+
:isin, :instrument_type, :expiry_flag, :bracket_flag, :cover_flag, :asm_gsm_flag,
|
12
|
+
:asm_gsm_category, :buy_sell_indicator, :buy_co_min_margin_per, :sell_co_min_margin_per,
|
13
|
+
:mtf_leverage
|
11
14
|
|
12
15
|
class << self
|
13
16
|
# @return [DhanHQ::Resources::Instruments]
|
@@ -29,6 +32,85 @@ module DhanHQ
|
|
29
32
|
rows.map { |r| new(normalize_csv_row(r), skip_validation: true) }
|
30
33
|
end
|
31
34
|
|
35
|
+
# Find a specific instrument by exchange segment and symbol.
|
36
|
+
# @param exchange_segment [String] The exchange segment (e.g., "NSE_EQ", "IDX_I")
|
37
|
+
# @param symbol [String] The symbol name to search for
|
38
|
+
# @param options [Hash] Additional search options
|
39
|
+
# @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
|
40
|
+
# @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
|
41
|
+
# @return [Instrument, nil] The found instrument or nil if not found
|
42
|
+
# @example
|
43
|
+
# # Find RELIANCE in NSE_EQ (uses underlying_symbol for equity)
|
44
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
|
45
|
+
# puts instrument.security_id # => "2885"
|
46
|
+
#
|
47
|
+
# # Find NIFTY in IDX_I (uses symbol_name for indices)
|
48
|
+
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
49
|
+
# puts instrument.security_id # => "13"
|
50
|
+
#
|
51
|
+
# # Exact match search
|
52
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE", exact_match: true)
|
53
|
+
#
|
54
|
+
# # Case sensitive search
|
55
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "reliance", case_sensitive: true)
|
56
|
+
def find(exchange_segment, symbol, options = { exact_match: true, case_sensitive: false })
|
57
|
+
validate_params!({ exchange_segment: exchange_segment, symbol: symbol }, DhanHQ::Contracts::InstrumentListContract)
|
58
|
+
|
59
|
+
exact_match = options[:exact_match] || false
|
60
|
+
case_sensitive = options[:case_sensitive] || false
|
61
|
+
|
62
|
+
instruments = by_segment(exchange_segment)
|
63
|
+
return nil if instruments.empty?
|
64
|
+
|
65
|
+
search_symbol = case_sensitive ? symbol : symbol.upcase
|
66
|
+
|
67
|
+
instruments.find do |instrument|
|
68
|
+
# For equity instruments, prefer underlying_symbol over symbol_name
|
69
|
+
instrument_symbol = if instrument.instrument == "EQUITY" && instrument.underlying_symbol
|
70
|
+
case_sensitive ? instrument.underlying_symbol : instrument.underlying_symbol.upcase
|
71
|
+
else
|
72
|
+
case_sensitive ? instrument.symbol_name : instrument.symbol_name.upcase
|
73
|
+
end
|
74
|
+
|
75
|
+
if exact_match
|
76
|
+
instrument_symbol == search_symbol
|
77
|
+
else
|
78
|
+
instrument_symbol.include?(search_symbol)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Find a specific instrument across all exchange segments.
|
84
|
+
# @param symbol [String] The symbol name to search for
|
85
|
+
# @param options [Hash] Additional search options
|
86
|
+
# @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
|
87
|
+
# @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
|
88
|
+
# @option options [Array<String>] :segments Specific segments to search in (default: all common segments)
|
89
|
+
# @return [Instrument, nil] The found instrument or nil if not found
|
90
|
+
# @example
|
91
|
+
# # Find RELIANCE across all segments
|
92
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE")
|
93
|
+
# puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "NSE_EQ:2885"
|
94
|
+
#
|
95
|
+
# # Find NIFTY across all segments
|
96
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("NIFTY")
|
97
|
+
# puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "IDX_I:13"
|
98
|
+
#
|
99
|
+
# # Search only in specific segments
|
100
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE", segments: ["NSE_EQ", "BSE_EQ"])
|
101
|
+
def find_anywhere(symbol, options = {})
|
102
|
+
exact_match = options[:exact_match] || false
|
103
|
+
case_sensitive = options[:case_sensitive] || false
|
104
|
+
segments = options[:segments] || %w[NSE_EQ BSE_EQ IDX_I NSE_FNO NSE_CURRENCY]
|
105
|
+
|
106
|
+
segments.each do |segment|
|
107
|
+
instrument = find(segment, symbol, exact_match: exact_match, case_sensitive: case_sensitive)
|
108
|
+
return instrument if instrument
|
109
|
+
end
|
110
|
+
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
32
114
|
def normalize_csv_row(row)
|
33
115
|
{
|
34
116
|
security_id: row["SECURITY_ID"].to_s,
|
@@ -41,7 +123,19 @@ module DhanHQ
|
|
41
123
|
tick_size: row["TICK_SIZE"]&.to_f,
|
42
124
|
expiry_date: row["SM_EXPIRY_DATE"],
|
43
125
|
strike_price: row["STRIKE_PRICE"]&.to_f,
|
44
|
-
option_type: row["OPTION_TYPE"]
|
126
|
+
option_type: row["OPTION_TYPE"],
|
127
|
+
underlying_symbol: row["UNDERLYING_SYMBOL"],
|
128
|
+
isin: row["ISIN"],
|
129
|
+
instrument_type: row["INSTRUMENT_TYPE"],
|
130
|
+
expiry_flag: row["EXPIRY_FLAG"],
|
131
|
+
bracket_flag: row["BRACKET_FLAG"],
|
132
|
+
cover_flag: row["COVER_FLAG"],
|
133
|
+
asm_gsm_flag: row["ASM_GSM_FLAG"],
|
134
|
+
asm_gsm_category: row["ASM_GSM_CATEGORY"],
|
135
|
+
buy_sell_indicator: row["BUY_SELL_INDICATOR"],
|
136
|
+
buy_co_min_margin_per: row["BUY_CO_MIN_MARGIN_PER"]&.to_f,
|
137
|
+
sell_co_min_margin_per: row["SELL_CO_MIN_MARGIN_PER"]&.to_f,
|
138
|
+
mtf_leverage: row["MTF_LEVERAGE"]&.to_f
|
45
139
|
}
|
46
140
|
end
|
47
141
|
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Represents a real-time order update received via WebSocket
|
7
|
+
# Parses and provides access to all order update fields as per DhanHQ API documentation
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
9
|
+
class OrderUpdate < BaseModel
|
10
|
+
# All order update attributes as per API documentation
|
11
|
+
attributes :exchange, :segment, :source, :security_id, :client_id,
|
12
|
+
:exch_order_no, :order_no, :product, :txn_type, :order_type,
|
13
|
+
:validity, :disc_quantity, :disc_qty_rem, :remaining_quantity,
|
14
|
+
:quantity, :traded_qty, :price, :trigger_price, :traded_price,
|
15
|
+
:avg_traded_price, :algo_ord_no, :off_mkt_flag, :order_date_time,
|
16
|
+
:exch_order_time, :last_updated_time, :remarks, :mkt_type,
|
17
|
+
:reason_description, :leg_no, :instrument, :symbol, :product_name,
|
18
|
+
:status, :lot_size, :strike_price, :expiry_date, :opt_type,
|
19
|
+
:display_name, :isin, :series, :good_till_days_date, :ref_ltp,
|
20
|
+
:tick_size, :algo_id, :multiplier, :correlation_id
|
21
|
+
|
22
|
+
##
|
23
|
+
# Create OrderUpdate from WebSocket message
|
24
|
+
# @param message [Hash] Raw WebSocket message
|
25
|
+
# @return [OrderUpdate] Parsed order update
|
26
|
+
def self.from_websocket_message(message)
|
27
|
+
return nil unless message.is_a?(Hash) && message[:Type] == "order_alert"
|
28
|
+
return nil unless message[:Data].is_a?(Hash)
|
29
|
+
|
30
|
+
# Map the WebSocket message data to our attributes
|
31
|
+
data = message[:Data]
|
32
|
+
new(data, skip_validation: true)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# OrderUpdate objects are read-only, so no validation contract needed
|
37
|
+
def validation_contract
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Helper methods for transaction type
|
43
|
+
def buy?
|
44
|
+
txn_type == "B"
|
45
|
+
end
|
46
|
+
|
47
|
+
def sell?
|
48
|
+
txn_type == "S"
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Helper methods for order type
|
53
|
+
def limit_order?
|
54
|
+
order_type == "LMT"
|
55
|
+
end
|
56
|
+
|
57
|
+
def market_order?
|
58
|
+
order_type == "MKT"
|
59
|
+
end
|
60
|
+
|
61
|
+
def stop_loss_order?
|
62
|
+
order_type == "SL"
|
63
|
+
end
|
64
|
+
|
65
|
+
def stop_loss_market_order?
|
66
|
+
order_type == "SLM"
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Helper methods for product type
|
71
|
+
def cnc_product?
|
72
|
+
product == "C"
|
73
|
+
end
|
74
|
+
|
75
|
+
def intraday_product?
|
76
|
+
product == "I"
|
77
|
+
end
|
78
|
+
|
79
|
+
def margin_product?
|
80
|
+
product == "M"
|
81
|
+
end
|
82
|
+
|
83
|
+
def mtf_product?
|
84
|
+
product == "F"
|
85
|
+
end
|
86
|
+
|
87
|
+
def cover_order?
|
88
|
+
product == "V"
|
89
|
+
end
|
90
|
+
|
91
|
+
def bracket_order?
|
92
|
+
product == "B"
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Helper methods for order status
|
97
|
+
def transit?
|
98
|
+
status == "TRANSIT"
|
99
|
+
end
|
100
|
+
|
101
|
+
def pending?
|
102
|
+
status == "PENDING"
|
103
|
+
end
|
104
|
+
|
105
|
+
def rejected?
|
106
|
+
status == "REJECTED"
|
107
|
+
end
|
108
|
+
|
109
|
+
def cancelled?
|
110
|
+
status == "CANCELLED"
|
111
|
+
end
|
112
|
+
|
113
|
+
def traded?
|
114
|
+
status == "TRADED"
|
115
|
+
end
|
116
|
+
|
117
|
+
def expired?
|
118
|
+
status == "EXPIRED"
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Helper methods for instrument type
|
123
|
+
def equity?
|
124
|
+
instrument == "EQUITY"
|
125
|
+
end
|
126
|
+
|
127
|
+
def derivative?
|
128
|
+
instrument == "DERIVATIVES"
|
129
|
+
end
|
130
|
+
|
131
|
+
def option?
|
132
|
+
%w[CE PE].include?(opt_type)
|
133
|
+
end
|
134
|
+
|
135
|
+
def call_option?
|
136
|
+
opt_type == "CE"
|
137
|
+
end
|
138
|
+
|
139
|
+
def put_option?
|
140
|
+
opt_type == "PE"
|
141
|
+
end
|
142
|
+
|
143
|
+
##
|
144
|
+
# Helper methods for order leg (for Super Orders)
|
145
|
+
def entry_leg?
|
146
|
+
leg_no == 1
|
147
|
+
end
|
148
|
+
|
149
|
+
def stop_loss_leg?
|
150
|
+
leg_no == 2
|
151
|
+
end
|
152
|
+
|
153
|
+
def target_leg?
|
154
|
+
leg_no == 3
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Helper methods for market type
|
159
|
+
def normal_market?
|
160
|
+
mkt_type == "NL"
|
161
|
+
end
|
162
|
+
|
163
|
+
def auction_market?
|
164
|
+
%w[AU A1 A2].include?(mkt_type)
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Helper methods for order characteristics
|
169
|
+
def amo_order?
|
170
|
+
off_mkt_flag == "1"
|
171
|
+
end
|
172
|
+
|
173
|
+
def super_order?
|
174
|
+
remarks == "Super Order"
|
175
|
+
end
|
176
|
+
|
177
|
+
def partially_executed?
|
178
|
+
traded_qty.positive? && traded_qty < quantity
|
179
|
+
end
|
180
|
+
|
181
|
+
def fully_executed?
|
182
|
+
traded_qty == quantity
|
183
|
+
end
|
184
|
+
|
185
|
+
def not_executed?
|
186
|
+
traded_qty.zero?
|
187
|
+
end
|
188
|
+
|
189
|
+
##
|
190
|
+
# Calculation methods
|
191
|
+
def execution_percentage
|
192
|
+
return 0.0 if quantity.zero?
|
193
|
+
|
194
|
+
(traded_qty.to_f / quantity * 100).round(2)
|
195
|
+
end
|
196
|
+
|
197
|
+
def pending_quantity
|
198
|
+
quantity - traded_qty
|
199
|
+
end
|
200
|
+
|
201
|
+
def total_value
|
202
|
+
return 0 unless traded_qty && avg_traded_price
|
203
|
+
|
204
|
+
traded_qty * avg_traded_price
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
# Status summary for logging/debugging
|
209
|
+
# rubocop:disable Metrics/MethodLength
|
210
|
+
def status_summary
|
211
|
+
{
|
212
|
+
order_no: order_no,
|
213
|
+
symbol: symbol,
|
214
|
+
status: status,
|
215
|
+
txn_type: txn_type,
|
216
|
+
quantity: quantity,
|
217
|
+
traded_qty: traded_qty,
|
218
|
+
execution_percentage: execution_percentage,
|
219
|
+
price: price,
|
220
|
+
avg_traded_price: avg_traded_price,
|
221
|
+
leg_no: leg_no,
|
222
|
+
super_order: super_order?
|
223
|
+
}
|
224
|
+
end
|
225
|
+
# rubocop:enable Metrics/MethodLength
|
226
|
+
|
227
|
+
##
|
228
|
+
# Convert to hash for serialization
|
229
|
+
def to_hash
|
230
|
+
@attributes.dup
|
231
|
+
end
|
232
|
+
end
|
233
|
+
# rubocop:enable Metrics/ClassLength
|
234
|
+
end
|
235
|
+
end
|