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
@@ -4,12 +4,14 @@ module DhanHQ
4
4
  module Models
5
5
  ##
6
6
  # Represents a single trade.
7
- # The API docs show an array of trades from GET /v2/trades/{from-date}/{to-date}/{page}
7
+ # Supports three main API endpoints:
8
+ # 1. GET /v2/trades - Current day trades
9
+ # 2. GET /v2/trades/{order-id} - Trades for specific order
10
+ # 3. GET /v2/trades/{from-date}/{to-date}/{page} - Historical trades
8
11
  class Trade < BaseModel
9
- # No explicit HTTP_PATH if we rely on the statements resource
10
- # but we can define it if needed
11
12
  HTTP_PATH = "/v2/trades"
12
13
 
14
+ # All trade attributes as per API documentation
13
15
  attributes :dhan_client_id, :order_id, :exchange_order_id, :exchange_trade_id,
14
16
  :transaction_type, :exchange_segment, :product_type, :order_type,
15
17
  :trading_symbol, :custom_symbol, :security_id, :traded_quantity,
@@ -19,14 +21,6 @@ module DhanHQ
19
21
  :drv_option_type, :drv_strike_price
20
22
 
21
23
  class << self
22
- ##
23
- # Provide a **shared instance** of the `Statements` resource,
24
- # where we have `trade_history(from_date:, to_date:, page:)`.
25
- # used for fetching historical trades.
26
- def resource
27
- @resource ||= DhanHQ::Resources::Statements.new
28
- end
29
-
30
24
  ##
31
25
  # Resource for current day tradebook APIs
32
26
  def tradebook_resource
@@ -34,46 +28,139 @@ module DhanHQ
34
28
  end
35
29
 
36
30
  ##
37
- # Fetch trades within the given date range and page.
38
- # GET /v2/trades/{from-date}/{to-date}/{page}
39
- #
40
- # @param from_date [String]
41
- # @param to_date [String]
42
- # @param page [Integer] Default 0
43
- # @return [Array<Trade>]
44
- # Retrieve historical trades
45
- def history(from_date:, to_date:, page: 0)
46
- # The resource call returns an Array<Hash>.
47
- response = resource.trade_history(from_date: from_date, to_date: to_date, page: page)
48
- return [] unless response.is_a?(Array)
49
-
50
- response.map { |t| new(t, skip_validation: true) }
31
+ # Resource for historical trade data
32
+ def statements_resource
33
+ @statements_resource ||= DhanHQ::Resources::Statements.new
51
34
  end
52
35
 
53
- alias all history
54
-
55
- # Retrieve current day trades
36
+ ##
37
+ # Fetch current day trades
38
+ # GET /v2/trades
39
+ #
40
+ # @return [Array<Trade>] Array of trades executed today
56
41
  def today
57
42
  response = tradebook_resource.all
58
43
  return [] unless response.is_a?(Array)
59
44
 
60
- response.map { |t| new(t, skip_validation: true) }
45
+ response.map { |trade_data| new(trade_data, skip_validation: true) }
61
46
  end
62
47
 
63
- # Fetch trades for a specific order id for the current day
48
+ ##
49
+ # Fetch trades for a specific order ID (current day)
50
+ # GET /v2/trades/{order-id}
51
+ #
52
+ # @param order_id [String] The order ID to fetch trades for
53
+ # @return [Trade, nil] Trade object or nil if not found
64
54
  def find_by_order_id(order_id)
55
+ # Validate input
56
+ contract = DhanHQ::Contracts::TradeByOrderIdContract.new
57
+ validation_result = contract.call(order_id: order_id)
58
+
59
+ unless validation_result.success?
60
+ raise DhanHQ::ValidationError, "Invalid order_id: #{validation_result.errors.to_h}"
61
+ end
62
+
65
63
  response = tradebook_resource.find(order_id)
66
64
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
67
65
 
68
66
  data = response.is_a?(Array) ? response.first : response
69
67
  new(data, skip_validation: true)
70
68
  end
69
+
70
+ ##
71
+ # Fetch historical trades within the given date range and page
72
+ # GET /v2/trades/{from-date}/{to-date}/{page}
73
+ #
74
+ # @param from_date [String] Start date in YYYY-MM-DD format
75
+ # @param to_date [String] End date in YYYY-MM-DD format
76
+ # @param page [Integer] Page number (default: 0)
77
+ # @return [Array<Trade>] Array of historical trades
78
+ def history(from_date:, to_date:, page: 0)
79
+ validate_history_params(from_date, to_date, page)
80
+
81
+ response = statements_resource.trade_history(
82
+ from_date: from_date,
83
+ to_date: to_date,
84
+ page: page
85
+ )
86
+
87
+ return [] unless response.is_a?(Array)
88
+
89
+ response.map { |trade_data| new(trade_data, skip_validation: true) }
90
+ end
91
+
92
+ private
93
+
94
+ def validate_history_params(from_date, to_date, page)
95
+ contract = DhanHQ::Contracts::TradeHistoryContract.new
96
+ validation_result = contract.call(from_date: from_date, to_date: to_date, page: page)
97
+
98
+ return if validation_result.success?
99
+
100
+ raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
101
+ end
102
+
103
+ # Alias for backward compatibility
104
+ alias all history
71
105
  end
72
106
 
73
- # If you want custom validations, you'd set a contract or skip for read-only
107
+ ##
108
+ # Trade objects are read-only, so no validation contract needed
74
109
  def validation_contract
75
110
  nil
76
111
  end
112
+
113
+ ##
114
+ # Helper methods for trade data
115
+ def buy?
116
+ transaction_type == "BUY"
117
+ end
118
+
119
+ def sell?
120
+ transaction_type == "SELL"
121
+ end
122
+
123
+ def equity?
124
+ instrument == "EQUITY"
125
+ end
126
+
127
+ def derivative?
128
+ instrument == "DERIVATIVES"
129
+ end
130
+
131
+ def option?
132
+ %w[CALL PUT].include?(drv_option_type)
133
+ end
134
+
135
+ def call_option?
136
+ drv_option_type == "CALL"
137
+ end
138
+
139
+ def put_option?
140
+ drv_option_type == "PUT"
141
+ end
142
+
143
+ ##
144
+ # Calculate total trade value
145
+ def total_value
146
+ return 0 unless traded_quantity && traded_price
147
+
148
+ traded_quantity * traded_price
149
+ end
150
+
151
+ ##
152
+ # Calculate total charges
153
+ def total_charges
154
+ charges = [sebi_tax, stt, brokerage_charges, service_tax,
155
+ exchange_transaction_charges, stamp_duty].compact
156
+ charges.sum(&:to_f)
157
+ end
158
+
159
+ ##
160
+ # Net trade value after charges
161
+ def net_value
162
+ total_value - total_charges
163
+ end
77
164
  end
78
165
  end
79
166
  end
@@ -33,9 +33,11 @@ module DhanHQ
33
33
  if @api_type == :option_chain
34
34
  last_request_time = @buckets[:last_request_time]
35
35
 
36
- sleep_time = 4 - (Time.now - last_request_time)
36
+ sleep_time = 3 - (Time.now - last_request_time)
37
37
  if sleep_time.positive?
38
- puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
38
+ if ENV["DHAN_DEBUG"] == "true"
39
+ puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
40
+ end
39
41
  sleep(sleep_time)
40
42
  end
41
43
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Resources
5
+ ##
6
+ # Resource for expired options data API endpoints
7
+ class ExpiredOptionsData < BaseAPI
8
+ API_TYPE = :data_api
9
+ HTTP_PATH = "/charts"
10
+
11
+ ##
12
+ # Fetch expired options data for rolling contracts
13
+ # POST /charts/rollingoption
14
+ #
15
+ # @param params [Hash] Parameters for the request
16
+ # @return [Hash] API response with expired options data
17
+ def fetch(params)
18
+ post("/rollingoption", params: params)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.3"
5
+ VERSION = "2.1.6"
6
6
  end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventmachine"
4
+ require "faye/websocket"
5
+ require "json"
6
+ require "concurrent"
7
+ require "uri"
8
+
9
+ module DhanHQ
10
+ module WS
11
+ ##
12
+ # Base WebSocket connection class providing common functionality
13
+ # for all DhanHQ WebSocket connections (Orders, Market Feed, Market Depth)
14
+ class BaseConnection
15
+ COOL_OFF_429 = 60 # seconds to cool off on 429
16
+ MAX_BACKOFF = 90 # cap exponential backoff
17
+
18
+ attr_reader :stopping, :url, :callbacks, :started
19
+
20
+ ##
21
+ # Initialize base connection
22
+ # @param url [String] WebSocket endpoint URL
23
+ # @param options [Hash] Connection options
24
+ # @option options [Integer] :max_backoff Maximum backoff time (default: 90)
25
+ # @option options [Integer] :cool_off_429 Cool off time for 429 errors (default: 60)
26
+ def initialize(url:, **options)
27
+ @url = url
28
+ @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
29
+ @started = Concurrent::AtomicBoolean.new(false)
30
+ @stop = false
31
+ @stopping = false
32
+ @ws = nil
33
+ @timer = nil
34
+ @cooloff_until = nil
35
+ @thr = nil
36
+ @max_backoff = options[:max_backoff] || MAX_BACKOFF
37
+ @cool_off_429 = options[:cool_off_429] || COOL_OFF_429
38
+ end
39
+
40
+ ##
41
+ # Start the WebSocket connection
42
+ # @return [BaseConnection] self for method chaining
43
+ def start
44
+ return self if @started.true?
45
+
46
+ @started.make_true
47
+ @thr = Thread.new { loop_run }
48
+ self
49
+ end
50
+
51
+ ##
52
+ # Stop the WebSocket connection gracefully
53
+ # @return [BaseConnection] self for method chaining
54
+ def stop
55
+ return self unless @started.true?
56
+
57
+ @started.make_false
58
+ @stop = true
59
+ @stopping = true
60
+ @ws&.close
61
+ self
62
+ end
63
+
64
+ ##
65
+ # Force disconnect the WebSocket
66
+ # @return [BaseConnection] self for method chaining
67
+ def disconnect!
68
+ @stop = true
69
+ @stopping = true
70
+ @ws&.close
71
+ self
72
+ end
73
+
74
+ ##
75
+ # Check if connection is open
76
+ # @return [Boolean] true if connected
77
+ def open?
78
+ @ws && @ws.instance_variable_get(:@driver)&.ready_state == 1
79
+ rescue StandardError
80
+ false
81
+ end
82
+
83
+ ##
84
+ # Check if connection is connected (alias for open?)
85
+ # @return [Boolean] true if connected
86
+ def connected?
87
+ open?
88
+ end
89
+
90
+ ##
91
+ # Register event handler
92
+ # @param event [Symbol] Event type
93
+ # @param block [Proc] Event handler
94
+ # @return [BaseConnection] self for method chaining
95
+ def on(event, &block)
96
+ @callbacks[event] << block
97
+ self
98
+ end
99
+
100
+ ##
101
+ # Emit event to registered callbacks
102
+ # @param event [Symbol] Event type
103
+ # @param payload [Object] Event payload
104
+ def emit(event, payload = nil)
105
+ list = @callbacks[event] || []
106
+ list.each { |cb| cb.call(payload) }
107
+ rescue StandardError => e
108
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Error in event handler: #{e.class} #{e.message}")
109
+ end
110
+
111
+ ##
112
+ # Send message over WebSocket
113
+ # @param message [String, Hash] Message to send
114
+ def send_message(message)
115
+ return unless @ws && open?
116
+
117
+ data = message.is_a?(Hash) ? message.to_json : message
118
+ @ws.send(data)
119
+ rescue StandardError => e
120
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Error sending message: #{e.class} #{e.message}")
121
+ end
122
+
123
+ private
124
+
125
+ ##
126
+ # Main connection loop with reconnection logic
127
+ def loop_run
128
+ backoff = 2.0
129
+ until @stop
130
+ failed = false
131
+ got_429 = false
132
+
133
+ # Respect any active cool-off window
134
+ sleep (@cooloff_until - Time.now).ceil if @cooloff_until && Time.now < @cooloff_until
135
+
136
+ begin
137
+ failed, got_429 = run_session
138
+ rescue StandardError => e
139
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Connection crashed: #{e.class} #{e.message}")
140
+ failed = true
141
+ ensure
142
+ break if @stop
143
+
144
+ if got_429
145
+ @cooloff_until = Time.now + @cool_off_429
146
+ DhanHQ.logger&.warn("[DhanHQ::WS::BaseConnection] Cooling off #{@cool_off_429}s due to 429")
147
+ end
148
+
149
+ if failed
150
+ sleep_time = [backoff, @max_backoff].min
151
+ jitter = rand(0.2 * sleep_time)
152
+ DhanHQ.logger&.warn("[DhanHQ::WS::BaseConnection] Reconnecting in #{(sleep_time + jitter).round(1)}s")
153
+ sleep(sleep_time + jitter)
154
+ backoff *= 2.0
155
+ else
156
+ backoff = 2.0
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ ##
163
+ # Run a single WebSocket session
164
+ # Must be implemented by subclasses
165
+ # @return [Array<Boolean>] [failed, got_429]
166
+ def run_session
167
+ raise NotImplementedError, "Subclasses must implement run_session"
168
+ end
169
+
170
+ ##
171
+ # Get default headers for WebSocket connection
172
+ # @return [Hash] Default headers
173
+ def default_headers
174
+ {
175
+ "User-Agent" => "DhanHQ-Ruby-Client/#{DhanHQ::VERSION}",
176
+ "Origin" => "https://dhanhq.co"
177
+ }
178
+ end
179
+
180
+ ##
181
+ # Sanitize URL for logging by removing sensitive parameters
182
+ # @param url [String] Original URL
183
+ # @return [String] Sanitized URL safe for logging
184
+ def sanitize_url(url)
185
+ return url if url.nil? || url.empty?
186
+
187
+ begin
188
+ uri = URI.parse(url)
189
+ # Remove sensitive query parameters
190
+ if uri.query
191
+ params = URI.decode_www_form(uri.query).reject do |key, _|
192
+ %w[token clientId client_id access_token].include?(key.downcase)
193
+ end
194
+ uri.query = URI.encode_www_form(params) unless params.empty?
195
+ end
196
+ uri.to_s
197
+ rescue StandardError
198
+ # If URL parsing fails, return a generic message
199
+ "wss://[sanitized-url]"
200
+ end
201
+ end
202
+
203
+ ##
204
+ # Handle WebSocket open event
205
+ def handle_open
206
+ DhanHQ.logger&.info("[DhanHQ::WS::BaseConnection] Connected to #{sanitize_url(@url)}")
207
+ emit(:open)
208
+ authenticate if respond_to?(:authenticate, true)
209
+ end
210
+
211
+ ##
212
+ # Handle WebSocket message event
213
+ # @param ev [Event] WebSocket message event
214
+ def handle_message(ev)
215
+ emit(:raw, ev.data)
216
+ process_message(ev.data) if respond_to?(:process_message, true)
217
+ end
218
+
219
+ ##
220
+ # Handle WebSocket close event
221
+ # @param ev [Event] WebSocket close event
222
+ def handle_close(ev)
223
+ @timer&.cancel
224
+ @timer = nil
225
+ msg = "[DhanHQ::WS::BaseConnection] Connection closed: #{ev.code} #{ev.reason}"
226
+ DhanHQ.logger&.warn(msg)
227
+
228
+ emit(:close, { code: ev.code, reason: ev.reason })
229
+
230
+ if @stopping
231
+ [false, false]
232
+ else
233
+ failed = (ev.code != 1000)
234
+ got_429 = ev.reason.to_s.include?("429")
235
+ [failed, got_429]
236
+ end
237
+ end
238
+
239
+ ##
240
+ # Handle WebSocket error event
241
+ # @param ev [Event] WebSocket error event
242
+ def handle_error(ev)
243
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] WebSocket error: #{ev.message}")
244
+ emit(:error, ev.message)
245
+ [true, false]
246
+ end
247
+ end
248
+ end
249
+ end
@@ -161,7 +161,7 @@ module DhanHQ
161
161
 
162
162
  private
163
163
 
164
- def prune(h) = { ExchangeSegment: h[:ExchangeSegment], SecurityId: h[:SecurityId] }
164
+ def prune(hash) = { ExchangeSegment: hash[:ExchangeSegment], SecurityId: hash[:SecurityId] }
165
165
 
166
166
  def emit(event, payload)
167
167
  begin
@@ -9,9 +9,9 @@ module DhanHQ
9
9
  # Low-level wrapper responsible for establishing and maintaining the raw
10
10
  # WebSocket connection to the streaming API.
11
11
  class Connection
12
- SUB_CODES = { ticker: 15, quote: 17, full: 21 }.freeze # adjust if needed
12
+ SUB_CODES = { ticker: 15, quote: 15, full: 15 }.freeze # All use RequestCode 15 per official API
13
13
  # Request codes used when unsubscribing from feeds.
14
- UNSUB_CODES = { ticker: 16, quote: 18, full: 22 }.freeze
14
+ UNSUB_CODES = { ticker: 12, quote: 12, full: 12 }.freeze # Use disconnect code 12 for unsubscribe
15
15
 
16
16
  COOL_OFF_429 = 60 # seconds to cool off on 429
17
17
  MAX_BACKOFF = 90 # cap exponential backoff
@@ -94,7 +94,7 @@ module DhanHQ
94
94
  backoff = 2.0
95
95
  until @stop
96
96
  failed = false
97
- got_429 = false # rubocop:disable Naming/VariableNumber
97
+ got_429 = false
98
98
 
99
99
  # respect any active cool-off window
100
100
  sleep (@cooloff_until - Time.now).ceil if @cooloff_until && Time.now < @cooloff_until
@@ -31,19 +31,19 @@ module DhanHQ
31
31
  when :ticker
32
32
  {
33
33
  kind: :ticker, segment: segstr, security_id: sid,
34
- ltp: pkt[:ltp].to_f, ts: pkt[:ltt].to_i
34
+ ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i
35
35
  }
36
36
  when :quote
37
37
  {
38
38
  kind: :quote, segment: segstr, security_id: sid,
39
- ltp: pkt[:ltp].to_f, ts: pkt[:ltt].to_i, atp: pkt[:atp].to_f,
39
+ ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i, atp: pkt[:atp].to_f,
40
40
  vol: pkt[:volume].to_i, ts_buy_qty: pkt[:total_buy_qty].to_i, ts_sell_qty: pkt[:total_sell_qty].to_i,
41
41
  day_open: pkt[:day_open]&.to_f, day_high: pkt[:day_high]&.to_f, day_low: pkt[:day_low]&.to_f, day_close: pkt[:day_close]&.to_f
42
42
  }
43
43
  when :full
44
44
  out = {
45
45
  kind: :full, segment: segstr, security_id: sid,
46
- ltp: pkt[:ltp].to_f, ts: pkt[:ltt].to_i, atp: pkt[:atp].to_f,
46
+ ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i, atp: pkt[:atp].to_f,
47
47
  vol: pkt[:volume].to_i, ts_buy_qty: pkt[:total_buy_qty].to_i, ts_sell_qty: pkt[:total_sell_qty].to_i,
48
48
  oi: pkt[:open_interest]&.to_i, oi_high: pkt[:highest_open_interest]&.to_i, oi_low: pkt[:lowest_open_interest]&.to_i,
49
49
  day_open: pkt[:day_open]&.to_f, day_high: pkt[:day_high]&.to_f, day_low: pkt[:day_low]&.to_f, day_close: pkt[:day_close]&.to_f