DhanHQ 2.1.7 → 2.1.10
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 +58 -0
- data/examples/comprehensive_websocket_examples.rb +0 -0
- data/examples/instrument_finder_test.rb +0 -0
- data/examples/live_order_updates.rb +1 -1
- data/examples/market_depth_example.rb +2 -2
- data/examples/order_update_example.rb +0 -0
- data/examples/trading_fields_example.rb +1 -1
- data/lib/DhanHQ/client.rb +2 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +6 -6
- data/lib/DhanHQ/core/base_model.rb +9 -1
- data/lib/DhanHQ/models/edis.rb +150 -14
- data/lib/DhanHQ/models/expired_options_data.rb +307 -82
- data/lib/DhanHQ/models/forever_order.rb +261 -22
- data/lib/DhanHQ/models/funds.rb +76 -10
- data/lib/DhanHQ/models/historical_data.rb +148 -31
- data/lib/DhanHQ/models/holding.rb +82 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +39 -5
- data/lib/DhanHQ/models/kill_switch.rb +113 -11
- data/lib/DhanHQ/models/ledger_entry.rb +101 -13
- data/lib/DhanHQ/models/margin.rb +133 -8
- data/lib/DhanHQ/models/market_feed.rb +181 -17
- data/lib/DhanHQ/models/option_chain.rb +184 -12
- data/lib/DhanHQ/models/order.rb +399 -34
- data/lib/DhanHQ/models/position.rb +161 -10
- data/lib/DhanHQ/models/profile.rb +103 -7
- data/lib/DhanHQ/models/super_order.rb +275 -15
- data/lib/DhanHQ/models/trade.rb +279 -26
- data/lib/DhanHQ/rate_limiter.rb +78 -23
- data/lib/DhanHQ/resources/expired_options_data.rb +1 -1
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +3 -1
- data/lib/DhanHQ/ws/orders/client.rb +2 -1
- data/lib/DhanHQ/ws/orders/connection.rb +0 -3
- data/lib/DhanHQ/ws/orders.rb +2 -1
- metadata +1 -1
data/lib/DhanHQ/models/trade.rb
CHANGED
|
@@ -3,11 +3,35 @@
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Models
|
|
5
5
|
##
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# Model for retrieving trade execution details.
|
|
7
|
+
#
|
|
8
|
+
# The Trade Book API lets you retrieve an array of all trades executed in a day.
|
|
9
|
+
# You can also fetch trade details for a specific order ID, which is useful during
|
|
10
|
+
# partial trades or Bracket/Cover Orders. Additionally, you can retrieve detailed
|
|
11
|
+
# trade history for all orders within a particular time frame.
|
|
12
|
+
#
|
|
13
|
+
# @example Fetch all trades for today
|
|
14
|
+
# trades = DhanHQ::Models::Trade.today
|
|
15
|
+
# trades.each do |trade|
|
|
16
|
+
# puts "#{trade.trading_symbol}: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
|
|
17
|
+
# puts "Total Value: ₹#{trade.total_value}, Charges: ₹#{trade.total_charges}"
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Fetch trades for a specific order
|
|
21
|
+
# trade = DhanHQ::Models::Trade.find_by_order_id("112111182045")
|
|
22
|
+
# if trade
|
|
23
|
+
# puts "Traded: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Fetch historical trades
|
|
27
|
+
# trades = DhanHQ::Models::Trade.history(
|
|
28
|
+
# from_date: "2022-12-01",
|
|
29
|
+
# to_date: "2022-12-31",
|
|
30
|
+
# page: 0
|
|
31
|
+
# )
|
|
32
|
+
# total_value = trades.sum(&:total_value)
|
|
33
|
+
# puts "Total traded value: ₹#{total_value}"
|
|
34
|
+
#
|
|
11
35
|
class Trade < BaseModel
|
|
12
36
|
HTTP_PATH = "/v2/trades"
|
|
13
37
|
|
|
@@ -22,22 +46,66 @@ module DhanHQ
|
|
|
22
46
|
|
|
23
47
|
class << self
|
|
24
48
|
##
|
|
25
|
-
#
|
|
49
|
+
# Provides a shared instance of the Trades resource for current day tradebook APIs.
|
|
50
|
+
#
|
|
51
|
+
# @return [DhanHQ::Resources::Trades] The Trades resource client instance
|
|
26
52
|
def tradebook_resource
|
|
27
53
|
@tradebook_resource ||= DhanHQ::Resources::Trades.new
|
|
28
54
|
end
|
|
29
55
|
|
|
30
56
|
##
|
|
31
|
-
#
|
|
57
|
+
# Provides a shared instance of the Statements resource for historical trade data.
|
|
58
|
+
#
|
|
59
|
+
# @return [DhanHQ::Resources::Statements] The Statements resource client instance
|
|
32
60
|
def statements_resource
|
|
33
61
|
@statements_resource ||= DhanHQ::Resources::Statements.new
|
|
34
62
|
end
|
|
35
63
|
|
|
36
64
|
##
|
|
37
|
-
#
|
|
38
|
-
#
|
|
65
|
+
# Retrieves all trades executed during the current trading day.
|
|
66
|
+
#
|
|
67
|
+
# Fetches an array of all trades executed in the day. This is useful for
|
|
68
|
+
# tracking daily execution activity and analyzing trade performance.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Trade>] Array of Trade objects. Returns empty array if no trades exist.
|
|
71
|
+
# Each Trade object contains (keys normalized to snake_case):
|
|
72
|
+
# - **:dhan_client_id** [String] User-specific identification generated by Dhan
|
|
73
|
+
# - **:order_id** [String] Order-specific identification generated by Dhan
|
|
74
|
+
# - **:exchange_order_id** [String] Order-specific identification generated by exchange
|
|
75
|
+
# - **:exchange_trade_id** [String] Trade-specific identification generated by exchange
|
|
76
|
+
# - **:transaction_type** [String] The trading side of transaction. "BUY" or "SELL"
|
|
77
|
+
# - **:exchange_segment** [String] Exchange segment of instrument
|
|
78
|
+
# - **:product_type** [String] Product type. Valid values: "CNC", "INTRADAY",
|
|
79
|
+
# "MARGIN", "MTF", "CO", "BO"
|
|
80
|
+
# - **:order_type** [String] Order type. Valid values: "LIMIT", "MARKET",
|
|
81
|
+
# "STOP_LOSS", "STOP_LOSS_MARKET"
|
|
82
|
+
# - **:trading_symbol** [String] Trading symbol of the instrument
|
|
83
|
+
# - **:security_id** [String] Exchange standard ID for each scrip
|
|
84
|
+
# - **:traded_quantity** [Integer] Number of shares executed
|
|
85
|
+
# - **:traded_price** [Float] Price at which trade is executed
|
|
86
|
+
# - **:create_time** [String] Time at which the order is created
|
|
87
|
+
# - **:update_time** [String] Time at which the last activity happened
|
|
88
|
+
# - **:exchange_time** [String] Time at which order reached at exchange
|
|
89
|
+
# - **:drv_expiry_date** [String, nil] For F&O, expiry date of contract
|
|
90
|
+
# - **:drv_option_type** [String, nil] Type of Option. "CALL" or "PUT"
|
|
91
|
+
# - **:drv_strike_price** [Float] For Options, Strike Price
|
|
92
|
+
#
|
|
93
|
+
# @example Fetch and analyze today's trades
|
|
94
|
+
# trades = DhanHQ::Models::Trade.today
|
|
95
|
+
# buy_trades = trades.select(&:buy?)
|
|
96
|
+
# sell_trades = trades.select(&:sell?)
|
|
97
|
+
# puts "Buy trades: #{buy_trades.count}, Sell trades: #{sell_trades.count}"
|
|
98
|
+
# total_value = trades.sum(&:total_value)
|
|
99
|
+
# puts "Total traded value: ₹#{total_value}"
|
|
100
|
+
#
|
|
101
|
+
# @example Calculate P&L for today
|
|
102
|
+
# trades = DhanHQ::Models::Trade.today
|
|
103
|
+
# buy_value = trades.select(&:buy?).sum(&:total_value)
|
|
104
|
+
# sell_value = trades.select(&:sell?).sum(&:total_value)
|
|
105
|
+
# total_charges = trades.sum(&:total_charges)
|
|
106
|
+
# pnl = sell_value - buy_value - total_charges
|
|
107
|
+
# puts "P&L: ₹#{pnl}"
|
|
39
108
|
#
|
|
40
|
-
# @return [Array<Trade>] Array of trades executed today
|
|
41
109
|
def today
|
|
42
110
|
response = tradebook_resource.all
|
|
43
111
|
return [] unless response.is_a?(Array)
|
|
@@ -46,11 +114,28 @@ module DhanHQ
|
|
|
46
114
|
end
|
|
47
115
|
|
|
48
116
|
##
|
|
49
|
-
#
|
|
50
|
-
#
|
|
117
|
+
# Retrieves trade details for a specific order ID (current day).
|
|
118
|
+
#
|
|
119
|
+
# Fetches all trades generated for a particular order ID. This is especially useful
|
|
120
|
+
# during partial trades or Bracket/Cover Orders where traders may get confused
|
|
121
|
+
# reading trades from the tradebook. The response includes all trades generated
|
|
122
|
+
# for the specified order ID.
|
|
123
|
+
#
|
|
124
|
+
# @param order_id [String] Order-specific identification generated by Dhan
|
|
51
125
|
#
|
|
52
|
-
# @
|
|
53
|
-
#
|
|
126
|
+
# @return [Trade, nil] Trade object with trade details if found, nil otherwise.
|
|
127
|
+
# Response structure is the same as {today}.
|
|
128
|
+
#
|
|
129
|
+
# @example Fetch trade for an order
|
|
130
|
+
# trade = DhanHQ::Models::Trade.find_by_order_id("112111182045")
|
|
131
|
+
# if trade
|
|
132
|
+
# puts "Order: #{trade.order_id}"
|
|
133
|
+
# puts "Traded: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
|
|
134
|
+
# puts "Symbol: #{trade.trading_symbol}"
|
|
135
|
+
# puts "Time: #{trade.exchange_time}"
|
|
136
|
+
# end
|
|
137
|
+
#
|
|
138
|
+
# @raise [DhanHQ::ValidationError] If order_id validation fails
|
|
54
139
|
def find_by_order_id(order_id)
|
|
55
140
|
# Validate input
|
|
56
141
|
contract = DhanHQ::Contracts::TradeByOrderIdContract.new
|
|
@@ -68,13 +153,61 @@ module DhanHQ
|
|
|
68
153
|
end
|
|
69
154
|
|
|
70
155
|
##
|
|
71
|
-
#
|
|
72
|
-
# GET /v2/trades/{from-date}/{to-date}/{page}
|
|
156
|
+
# Retrieves detailed trade history for all orders within a specified time frame.
|
|
73
157
|
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
158
|
+
# Fetches paginated trade history data with comprehensive charge breakdowns.
|
|
159
|
+
# The response includes detailed information about SEBI tax, STT, brokerage charges,
|
|
160
|
+
# service tax, exchange transaction charges, and stamp duty for each trade.
|
|
161
|
+
#
|
|
162
|
+
# @param from_date [String] (required) Start date in "YYYY-MM-DD" format
|
|
163
|
+
# @param to_date [String] (required) End date in "YYYY-MM-DD" format
|
|
164
|
+
# @param page [Integer] (default: 0) Page number for paginated results. Pass 0 for first page
|
|
165
|
+
#
|
|
166
|
+
# @return [Array<Trade>] Array of historical Trade objects. Returns empty array if no trades found.
|
|
167
|
+
# Each Trade object contains all fields from {today} plus additional charge details:
|
|
168
|
+
# - **:custom_symbol** [String] Trading Symbol as per Dhan
|
|
169
|
+
# - **:isin** [String] Universal standard ID for each scrip (International Securities Identification Number)
|
|
170
|
+
# - **:instrument** [String] Type of Instrument. "EQUITY" or "DERIVATIVES"
|
|
171
|
+
# - **:sebi_tax** [String] SEBI Turnover Charges
|
|
172
|
+
# - **:stt** [String] Securities Transactions Tax
|
|
173
|
+
# - **:brokerage_charges** [String] Brokerage charges by Dhan
|
|
174
|
+
# - **:service_tax** [String] Applicable Service Tax
|
|
175
|
+
# - **:exchange_transaction_charges** [String] Exchange Transaction Charge
|
|
176
|
+
# - **:stamp_duty** [String] Stamp Duty Charges
|
|
177
|
+
#
|
|
178
|
+
# @example Fetch historical trades for a month
|
|
179
|
+
# trades = DhanHQ::Models::Trade.history(
|
|
180
|
+
# from_date: "2022-12-01",
|
|
181
|
+
# to_date: "2022-12-31",
|
|
182
|
+
# page: 0
|
|
183
|
+
# )
|
|
184
|
+
# puts "Total trades: #{trades.count}"
|
|
185
|
+
#
|
|
186
|
+
# @example Calculate total charges for historical period
|
|
187
|
+
# trades = DhanHQ::Models::Trade.history(
|
|
188
|
+
# from_date: "2022-12-01",
|
|
189
|
+
# to_date: "2022-12-31"
|
|
190
|
+
# )
|
|
191
|
+
# total_charges = trades.sum(&:total_charges)
|
|
192
|
+
# total_brokerage = trades.sum { |t| t.brokerage_charges.to_f }
|
|
193
|
+
# puts "Total Charges: ₹#{total_charges}"
|
|
194
|
+
# puts "Total Brokerage: ₹#{total_brokerage}"
|
|
195
|
+
#
|
|
196
|
+
# @example Paginate through historical trades
|
|
197
|
+
# page = 0
|
|
198
|
+
# loop do
|
|
199
|
+
# trades = DhanHQ::Models::Trade.history(
|
|
200
|
+
# from_date: "2022-12-01",
|
|
201
|
+
# to_date: "2022-12-31",
|
|
202
|
+
# page: page
|
|
203
|
+
# )
|
|
204
|
+
# break if trades.empty?
|
|
205
|
+
#
|
|
206
|
+
# puts "Page #{page}: #{trades.count} trades"
|
|
207
|
+
# page += 1
|
|
208
|
+
# end
|
|
209
|
+
#
|
|
210
|
+
# @raise [DhanHQ::ValidationError] If date format or page validation fails
|
|
78
211
|
def history(from_date:, to_date:, page: 0)
|
|
79
212
|
validate_history_params(from_date, to_date, page)
|
|
80
213
|
|
|
@@ -91,6 +224,16 @@ module DhanHQ
|
|
|
91
224
|
|
|
92
225
|
private
|
|
93
226
|
|
|
227
|
+
##
|
|
228
|
+
# Validates parameters for historical trade queries.
|
|
229
|
+
#
|
|
230
|
+
# @param from_date [String] Start date in YYYY-MM-DD format
|
|
231
|
+
# @param to_date [String] End date in YYYY-MM-DD format
|
|
232
|
+
# @param page [Integer] Page number
|
|
233
|
+
#
|
|
234
|
+
# @raise [DhanHQ::ValidationError] If validation fails
|
|
235
|
+
#
|
|
236
|
+
# @api private
|
|
94
237
|
def validate_history_params(from_date, to_date, page)
|
|
95
238
|
contract = DhanHQ::Contracts::TradeHistoryContract.new
|
|
96
239
|
validation_result = contract.call(from_date: from_date, to_date: to_date, page: page)
|
|
@@ -100,48 +243,131 @@ module DhanHQ
|
|
|
100
243
|
raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
|
|
101
244
|
end
|
|
102
245
|
|
|
103
|
-
# Alias for backward compatibility
|
|
246
|
+
# Alias for backward compatibility with historical trades
|
|
247
|
+
# @api private
|
|
104
248
|
alias all history
|
|
105
249
|
end
|
|
106
250
|
|
|
107
251
|
##
|
|
108
|
-
# Trade objects are read-only, so no validation contract needed
|
|
252
|
+
# Trade objects are read-only, so no validation contract needed.
|
|
253
|
+
#
|
|
254
|
+
# @return [nil] Always returns nil as trades are read-only
|
|
255
|
+
#
|
|
256
|
+
# @api private
|
|
109
257
|
def validation_contract
|
|
110
258
|
nil
|
|
111
259
|
end
|
|
112
260
|
|
|
113
261
|
##
|
|
114
|
-
#
|
|
262
|
+
# Checks if the trade is a BUY transaction.
|
|
263
|
+
#
|
|
264
|
+
# @return [Boolean] true if transaction_type is "BUY", false otherwise
|
|
265
|
+
#
|
|
266
|
+
# @example
|
|
267
|
+
# trade = DhanHQ::Models::Trade.today.first
|
|
268
|
+
# if trade.buy?
|
|
269
|
+
# puts "This is a buy trade"
|
|
270
|
+
# end
|
|
271
|
+
#
|
|
115
272
|
def buy?
|
|
116
273
|
transaction_type == "BUY"
|
|
117
274
|
end
|
|
118
275
|
|
|
276
|
+
##
|
|
277
|
+
# Checks if the trade is a SELL transaction.
|
|
278
|
+
#
|
|
279
|
+
# @return [Boolean] true if transaction_type is "SELL", false otherwise
|
|
280
|
+
#
|
|
281
|
+
# @example
|
|
282
|
+
# trade = DhanHQ::Models::Trade.today.first
|
|
283
|
+
# if trade.sell?
|
|
284
|
+
# puts "This is a sell trade"
|
|
285
|
+
# end
|
|
286
|
+
#
|
|
119
287
|
def sell?
|
|
120
288
|
transaction_type == "SELL"
|
|
121
289
|
end
|
|
122
290
|
|
|
291
|
+
##
|
|
292
|
+
# Checks if the trade instrument is EQUITY.
|
|
293
|
+
#
|
|
294
|
+
# @return [Boolean] true if instrument is "EQUITY", false otherwise
|
|
295
|
+
#
|
|
296
|
+
# @example
|
|
297
|
+
# trades = DhanHQ::Models::Trade.today
|
|
298
|
+
# equity_trades = trades.select(&:equity?)
|
|
299
|
+
# puts "Equity trades: #{equity_trades.count}"
|
|
300
|
+
#
|
|
123
301
|
def equity?
|
|
124
302
|
instrument == "EQUITY"
|
|
125
303
|
end
|
|
126
304
|
|
|
305
|
+
##
|
|
306
|
+
# Checks if the trade instrument is DERIVATIVES.
|
|
307
|
+
#
|
|
308
|
+
# @return [Boolean] true if instrument is "DERIVATIVES", false otherwise
|
|
309
|
+
#
|
|
310
|
+
# @example
|
|
311
|
+
# trades = DhanHQ::Models::Trade.today
|
|
312
|
+
# derivative_trades = trades.select(&:derivative?)
|
|
313
|
+
# puts "Derivative trades: #{derivative_trades.count}"
|
|
314
|
+
#
|
|
127
315
|
def derivative?
|
|
128
316
|
instrument == "DERIVATIVES"
|
|
129
317
|
end
|
|
130
318
|
|
|
319
|
+
##
|
|
320
|
+
# Checks if the trade is an option (CALL or PUT).
|
|
321
|
+
#
|
|
322
|
+
# @return [Boolean] true if drv_option_type is "CALL" or "PUT", false otherwise
|
|
323
|
+
#
|
|
324
|
+
# @example
|
|
325
|
+
# trades = DhanHQ::Models::Trade.today
|
|
326
|
+
# option_trades = trades.select(&:option?)
|
|
327
|
+
# puts "Option trades: #{option_trades.count}"
|
|
328
|
+
#
|
|
131
329
|
def option?
|
|
132
330
|
%w[CALL PUT].include?(drv_option_type)
|
|
133
331
|
end
|
|
134
332
|
|
|
333
|
+
##
|
|
334
|
+
# Checks if the trade is a CALL option.
|
|
335
|
+
#
|
|
336
|
+
# @return [Boolean] true if drv_option_type is "CALL", false otherwise
|
|
337
|
+
#
|
|
338
|
+
# @example
|
|
339
|
+
# trades = DhanHQ::Models::Trade.today
|
|
340
|
+
# call_trades = trades.select(&:call_option?)
|
|
341
|
+
# puts "Call option trades: #{call_trades.count}"
|
|
342
|
+
#
|
|
135
343
|
def call_option?
|
|
136
344
|
drv_option_type == "CALL"
|
|
137
345
|
end
|
|
138
346
|
|
|
347
|
+
##
|
|
348
|
+
# Checks if the trade is a PUT option.
|
|
349
|
+
#
|
|
350
|
+
# @return [Boolean] true if drv_option_type is "PUT", false otherwise
|
|
351
|
+
#
|
|
352
|
+
# @example
|
|
353
|
+
# trades = DhanHQ::Models::Trade.today
|
|
354
|
+
# put_trades = trades.select(&:put_option?)
|
|
355
|
+
# puts "Put option trades: #{put_trades.count}"
|
|
356
|
+
#
|
|
139
357
|
def put_option?
|
|
140
358
|
drv_option_type == "PUT"
|
|
141
359
|
end
|
|
142
360
|
|
|
143
361
|
##
|
|
144
|
-
#
|
|
362
|
+
# Calculates the total trade value (quantity × price).
|
|
363
|
+
#
|
|
364
|
+
# @return [Float] Total trade value. Returns 0 if traded_quantity or traded_price is missing
|
|
365
|
+
#
|
|
366
|
+
# @example
|
|
367
|
+
# trade = DhanHQ::Models::Trade.today.first
|
|
368
|
+
# puts "Trade Value: ₹#{trade.total_value}"
|
|
369
|
+
# # => Trade Value: ₹133832.0
|
|
370
|
+
#
|
|
145
371
|
def total_value
|
|
146
372
|
return 0 unless traded_quantity && traded_price
|
|
147
373
|
|
|
@@ -149,7 +375,22 @@ module DhanHQ
|
|
|
149
375
|
end
|
|
150
376
|
|
|
151
377
|
##
|
|
152
|
-
#
|
|
378
|
+
# Calculates the total charges for the trade.
|
|
379
|
+
#
|
|
380
|
+
# Sums all applicable charges including SEBI tax, STT, brokerage charges,
|
|
381
|
+
# service tax, exchange transaction charges, and stamp duty.
|
|
382
|
+
#
|
|
383
|
+
# @return [Float] Total charges amount. Returns 0 if no charges are present
|
|
384
|
+
#
|
|
385
|
+
# @example
|
|
386
|
+
# trade = DhanHQ::Models::Trade.history(
|
|
387
|
+
# from_date: "2022-12-01",
|
|
388
|
+
# to_date: "2022-12-31"
|
|
389
|
+
# ).first
|
|
390
|
+
# puts "Total Charges: ₹#{trade.total_charges}"
|
|
391
|
+
# puts "Brokerage: ₹#{trade.brokerage_charges}"
|
|
392
|
+
# puts "STT: ₹#{trade.stt}"
|
|
393
|
+
#
|
|
153
394
|
def total_charges
|
|
154
395
|
charges = [sebi_tax, stt, brokerage_charges, service_tax,
|
|
155
396
|
exchange_transaction_charges, stamp_duty].compact
|
|
@@ -157,7 +398,19 @@ module DhanHQ
|
|
|
157
398
|
end
|
|
158
399
|
|
|
159
400
|
##
|
|
160
|
-
#
|
|
401
|
+
# Calculates the net trade value after deducting all charges.
|
|
402
|
+
#
|
|
403
|
+
# @return [Float] Net trade value (total_value - total_charges)
|
|
404
|
+
#
|
|
405
|
+
# @example
|
|
406
|
+
# trade = DhanHQ::Models::Trade.history(
|
|
407
|
+
# from_date: "2022-12-01",
|
|
408
|
+
# to_date: "2022-12-31"
|
|
409
|
+
# ).first
|
|
410
|
+
# puts "Gross Value: ₹#{trade.total_value}"
|
|
411
|
+
# puts "Charges: ₹#{trade.total_charges}"
|
|
412
|
+
# puts "Net Value: ₹#{trade.net_value}"
|
|
413
|
+
#
|
|
161
414
|
def net_value
|
|
162
415
|
total_value - total_charges
|
|
163
416
|
end
|
data/lib/DhanHQ/rate_limiter.rb
CHANGED
|
@@ -15,13 +15,31 @@ module DhanHQ
|
|
|
15
15
|
per_day: Float::INFINITY }
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
|
+
# Thread-safe shared rate limiters per API type
|
|
19
|
+
@shared_limiters = Concurrent::Map.new
|
|
20
|
+
@mutexes = Concurrent::Map.new
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_reader :shared_limiters, :mutexes
|
|
24
|
+
|
|
25
|
+
# Get or create a shared rate limiter instance for the given API type
|
|
26
|
+
def for(api_type)
|
|
27
|
+
@shared_limiters[api_type] ||= new(api_type)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
# Creates a rate limiter for a given API type.
|
|
32
|
+
# Note: For proper rate limiting coordination, use RateLimiter.for(api_type) instead
|
|
33
|
+
# of RateLimiter.new(api_type) to get a shared instance.
|
|
19
34
|
#
|
|
20
35
|
# @param api_type [Symbol] One of the keys from {RATE_LIMITS}.
|
|
21
36
|
def initialize(api_type)
|
|
22
37
|
@api_type = api_type
|
|
23
38
|
@buckets = Concurrent::Hash.new
|
|
24
39
|
@buckets[:last_request_time] = Time.at(0) if api_type == :option_chain
|
|
40
|
+
# Track request timestamps for per-second limiting
|
|
41
|
+
@request_times = []
|
|
42
|
+
@window_start = Time.now
|
|
25
43
|
initialize_buckets
|
|
26
44
|
start_cleanup_threads
|
|
27
45
|
end
|
|
@@ -30,34 +48,70 @@ module DhanHQ
|
|
|
30
48
|
#
|
|
31
49
|
# @return [void]
|
|
32
50
|
def throttle!
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
mutex.synchronize do
|
|
52
|
+
if @api_type == :option_chain
|
|
53
|
+
last_request_time = @buckets[:last_request_time]
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
sleep_time = 3 - (Time.now - last_request_time)
|
|
56
|
+
if sleep_time.positive?
|
|
57
|
+
if ENV["DHAN_DEBUG"] == "true"
|
|
58
|
+
puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
|
|
59
|
+
end
|
|
60
|
+
sleep(sleep_time)
|
|
40
61
|
end
|
|
41
|
-
|
|
62
|
+
|
|
63
|
+
@buckets[:last_request_time] = Time.now
|
|
64
|
+
return
|
|
42
65
|
end
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
# For per-second limits, use timestamp-based sliding window
|
|
68
|
+
per_second_limit = RATE_LIMITS[@api_type][:per_second]
|
|
69
|
+
if per_second_limit && per_second_limit != Float::INFINITY
|
|
70
|
+
now = Time.now
|
|
71
|
+
# Remove requests older than 1 second
|
|
72
|
+
@request_times.reject! { |t| now - t >= 1.0 }
|
|
73
|
+
|
|
74
|
+
# Check if we've hit the per-second limit
|
|
75
|
+
if @request_times.size >= per_second_limit
|
|
76
|
+
# Calculate how long to wait until the oldest request is 1 second old
|
|
77
|
+
oldest_time = @request_times.min
|
|
78
|
+
wait_time = 1.0 - (now - oldest_time)
|
|
79
|
+
|
|
80
|
+
if wait_time.positive?
|
|
81
|
+
sleep(wait_time)
|
|
82
|
+
# Recalculate after sleep
|
|
83
|
+
now = Time.now
|
|
84
|
+
@request_times.reject! { |t| now - t >= 1.0 }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
47
87
|
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
# Record this request time
|
|
89
|
+
@request_times << Time.now
|
|
90
|
+
end
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
# Check other limits (per_minute, per_hour, per_day)
|
|
93
|
+
loop do
|
|
94
|
+
break if allow_request?
|
|
95
|
+
|
|
96
|
+
sleep(0.1)
|
|
97
|
+
end
|
|
98
|
+
record_request
|
|
52
99
|
end
|
|
53
|
-
record_request
|
|
54
100
|
end
|
|
55
101
|
|
|
56
102
|
private
|
|
57
103
|
|
|
104
|
+
# Gets or creates a mutex for this API type for thread-safe throttling
|
|
105
|
+
def mutex
|
|
106
|
+
self.class.mutexes[@api_type] ||= Mutex.new
|
|
107
|
+
end
|
|
108
|
+
|
|
58
109
|
# Prepares the counters used for each interval in {RATE_LIMITS}.
|
|
59
110
|
def initialize_buckets
|
|
60
111
|
RATE_LIMITS[@api_type].each_key do |interval|
|
|
112
|
+
# Skip per_second as we handle it with timestamps
|
|
113
|
+
next if interval == :per_second
|
|
114
|
+
|
|
61
115
|
@buckets[interval] = Concurrent::AtomicFixnum.new(0)
|
|
62
116
|
end
|
|
63
117
|
end
|
|
@@ -67,6 +121,9 @@ module DhanHQ
|
|
|
67
121
|
# @return [Boolean]
|
|
68
122
|
def allow_request?
|
|
69
123
|
RATE_LIMITS[@api_type].all? do |interval, limit|
|
|
124
|
+
# Skip per_second check as it's handled in throttle! with timestamps
|
|
125
|
+
next true if interval == :per_second
|
|
126
|
+
|
|
70
127
|
@buckets[interval].value < limit
|
|
71
128
|
end
|
|
72
129
|
end
|
|
@@ -74,34 +131,32 @@ module DhanHQ
|
|
|
74
131
|
# Increments the counters for each time window once a request is made.
|
|
75
132
|
def record_request
|
|
76
133
|
RATE_LIMITS[@api_type].each_key do |interval|
|
|
134
|
+
# Skip per_second as it's handled with timestamps
|
|
135
|
+
next if interval == :per_second
|
|
136
|
+
|
|
77
137
|
@buckets[interval].increment
|
|
78
138
|
end
|
|
79
139
|
end
|
|
80
140
|
|
|
81
141
|
# Spawns background threads to reset counters after each interval elapses.
|
|
82
142
|
def start_cleanup_threads
|
|
83
|
-
|
|
84
|
-
loop do
|
|
85
|
-
sleep(1)
|
|
86
|
-
@buckets[:per_second].value = 0
|
|
87
|
-
end
|
|
88
|
-
end
|
|
143
|
+
# Don't create per_second cleanup thread - we handle it with timestamps
|
|
89
144
|
Thread.new do
|
|
90
145
|
loop do
|
|
91
146
|
sleep(60)
|
|
92
|
-
@buckets[:per_minute]
|
|
147
|
+
@buckets[:per_minute]&.value = 0
|
|
93
148
|
end
|
|
94
149
|
end
|
|
95
150
|
Thread.new do
|
|
96
151
|
loop do
|
|
97
152
|
sleep(3600)
|
|
98
|
-
@buckets[:per_hour]
|
|
153
|
+
@buckets[:per_hour]&.value = 0
|
|
99
154
|
end
|
|
100
155
|
end
|
|
101
156
|
Thread.new do
|
|
102
157
|
loop do
|
|
103
158
|
sleep(86_400)
|
|
104
|
-
@buckets[:per_day]
|
|
159
|
+
@buckets[:per_day]&.value = 0
|
|
105
160
|
end
|
|
106
161
|
end
|
|
107
162
|
end
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -213,7 +213,9 @@ module DhanHQ
|
|
|
213
213
|
|
|
214
214
|
instrument = find_instrument(symbol_code, segment_hint)
|
|
215
215
|
unless instrument
|
|
216
|
-
DhanHQ.logger&.warn(
|
|
216
|
+
DhanHQ.logger&.warn(
|
|
217
|
+
"[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || "AUTO"})"
|
|
218
|
+
)
|
|
217
219
|
return nil
|
|
218
220
|
end
|
|
219
221
|
|
|
@@ -184,8 +184,9 @@ module DhanHQ
|
|
|
184
184
|
|
|
185
185
|
##
|
|
186
186
|
# Emit status-specific events
|
|
187
|
+
#
|
|
187
188
|
# @param order_update [OrderUpdate] Current order update
|
|
188
|
-
# @param
|
|
189
|
+
# @param _previous_state [OrderUpdate, nil] Previous order state (unused parameter)
|
|
189
190
|
# rubocop:disable Metrics/MethodLength
|
|
190
191
|
def emit_status_specific_events(order_update, _previous_state)
|
|
191
192
|
case order_update.status
|
data/lib/DhanHQ/ws/orders.rb
CHANGED
|
@@ -10,7 +10,8 @@ module DhanHQ
|
|
|
10
10
|
module Orders
|
|
11
11
|
##
|
|
12
12
|
# Connect to order updates WebSocket with a simple callback
|
|
13
|
-
#
|
|
13
|
+
#
|
|
14
|
+
# @yield [OrderUpdate] Yields order update objects when received
|
|
14
15
|
# @return [Client] WebSocket client instance
|
|
15
16
|
def self.connect(&)
|
|
16
17
|
Client.new.start.on(:update, &)
|