DhanHQ 2.1.8 → 2.2.0
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 +1 -1
- data/.rubocop_todo.yml +143 -118
- data/CHANGELOG.md +177 -0
- data/CODE_REVIEW_ISSUES.md +397 -0
- data/FIXES_APPLIED.md +373 -0
- data/GUIDE.md +41 -0
- data/README.md +55 -0
- data/RELEASING.md +60 -0
- data/REVIEW_SUMMARY.md +120 -0
- data/VERSION_UPDATE.md +82 -0
- data/core +0 -0
- data/docs/AUTHENTICATION.md +63 -0
- data/docs/DATA_API_PARAMETERS.md +278 -0
- data/docs/PR_2.2.0.md +48 -0
- data/docs/RELEASE_GUIDE.md +492 -0
- data/docs/TESTING_GUIDE.md +1514 -0
- data/docs/live_order_updates.md +25 -1
- data/docs/rails_integration.md +29 -0
- data/docs/websocket_integration.md +22 -0
- data/lib/DhanHQ/client.rb +65 -9
- data/lib/DhanHQ/configuration.rb +26 -0
- data/lib/DhanHQ/constants.rb +1 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +6 -6
- data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
- data/lib/DhanHQ/core/base_model.rb +26 -11
- data/lib/DhanHQ/errors.rb +4 -0
- data/lib/DhanHQ/helpers/request_helper.rb +17 -2
- data/lib/DhanHQ/helpers/response_helper.rb +34 -13
- data/lib/DhanHQ/models/edis.rb +150 -14
- data/lib/DhanHQ/models/expired_options_data.rb +307 -88
- 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 +4 -4
- 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 +418 -36
- data/lib/DhanHQ/models/order_update.rb +0 -4
- 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 +40 -6
- data/lib/DhanHQ/resources/expired_options_data.rb +1 -1
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +11 -5
- data/lib/DhanHQ/ws/connection.rb +16 -2
- data/lib/DhanHQ/ws/market_depth/client.rb +2 -1
- data/lib/DhanHQ/ws/market_depth.rb +12 -12
- data/lib/DhanHQ/ws/orders/client.rb +78 -12
- data/lib/DhanHQ/ws/orders/connection.rb +2 -1
- data/lib/DhanHQ/ws/orders.rb +2 -1
- metadata +18 -5
- data/lib/DhanHQ/contracts/modify_order_contract_copy.rb +0 -100
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
|
@@ -140,25 +140,59 @@ module DhanHQ
|
|
|
140
140
|
|
|
141
141
|
# Spawns background threads to reset counters after each interval elapses.
|
|
142
142
|
def start_cleanup_threads
|
|
143
|
+
@cleanup_threads = []
|
|
144
|
+
@shutdown = Concurrent::AtomicBoolean.new(false)
|
|
145
|
+
|
|
143
146
|
# Don't create per_second cleanup thread - we handle it with timestamps
|
|
144
|
-
Thread.new do
|
|
147
|
+
@cleanup_threads << Thread.new do
|
|
145
148
|
loop do
|
|
149
|
+
break if @shutdown.true?
|
|
150
|
+
|
|
146
151
|
sleep(60)
|
|
147
|
-
|
|
152
|
+
break if @shutdown.true?
|
|
153
|
+
|
|
154
|
+
mutex.synchronize do
|
|
155
|
+
@buckets[:per_minute]&.value = 0
|
|
156
|
+
end
|
|
148
157
|
end
|
|
149
158
|
end
|
|
150
|
-
|
|
159
|
+
|
|
160
|
+
@cleanup_threads << Thread.new do
|
|
151
161
|
loop do
|
|
162
|
+
break if @shutdown.true?
|
|
163
|
+
|
|
152
164
|
sleep(3600)
|
|
153
|
-
|
|
165
|
+
break if @shutdown.true?
|
|
166
|
+
|
|
167
|
+
mutex.synchronize do
|
|
168
|
+
@buckets[:per_hour]&.value = 0
|
|
169
|
+
end
|
|
154
170
|
end
|
|
155
171
|
end
|
|
156
|
-
|
|
172
|
+
|
|
173
|
+
@cleanup_threads << Thread.new do
|
|
157
174
|
loop do
|
|
175
|
+
break if @shutdown.true?
|
|
176
|
+
|
|
158
177
|
sleep(86_400)
|
|
159
|
-
|
|
178
|
+
break if @shutdown.true?
|
|
179
|
+
|
|
180
|
+
mutex.synchronize do
|
|
181
|
+
@buckets[:per_day]&.value = 0
|
|
182
|
+
end
|
|
160
183
|
end
|
|
161
184
|
end
|
|
162
185
|
end
|
|
186
|
+
|
|
187
|
+
# Shuts down cleanup threads gracefully
|
|
188
|
+
def shutdown
|
|
189
|
+
return if @shutdown.true?
|
|
190
|
+
|
|
191
|
+
@shutdown.make_true
|
|
192
|
+
@cleanup_threads&.each do |thread|
|
|
193
|
+
thread&.wakeup if thread&.alive?
|
|
194
|
+
thread&.join(5) # Wait up to 5 seconds for thread to finish
|
|
195
|
+
end
|
|
196
|
+
end
|
|
163
197
|
end
|
|
164
198
|
end
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -27,7 +27,8 @@ module DhanHQ
|
|
|
27
27
|
@callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
|
|
28
28
|
@started = Concurrent::AtomicBoolean.new(false)
|
|
29
29
|
|
|
30
|
-
token = DhanHQ.configuration.
|
|
30
|
+
token = DhanHQ.configuration.resolved_access_token
|
|
31
|
+
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
31
32
|
cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
|
|
32
33
|
ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
|
|
33
34
|
@url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
@@ -164,11 +165,16 @@ module DhanHQ
|
|
|
164
165
|
def prune(hash) = { ExchangeSegment: hash[:ExchangeSegment], SecurityId: hash[:SecurityId] }
|
|
165
166
|
|
|
166
167
|
def emit(event, payload)
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
# Create a frozen snapshot of callbacks to avoid modification during iteration
|
|
169
|
+
callbacks_snapshot = begin
|
|
170
|
+
@callbacks[event].dup.freeze
|
|
169
171
|
rescue StandardError
|
|
170
|
-
[]
|
|
171
|
-
end
|
|
172
|
+
[].freeze
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
callbacks_snapshot.each { |cb| cb.call(payload) }
|
|
176
|
+
rescue StandardError => e
|
|
177
|
+
DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
|
|
172
178
|
end
|
|
173
179
|
|
|
174
180
|
def install_at_exit_once!
|
data/lib/DhanHQ/ws/connection.rb
CHANGED
|
@@ -141,8 +141,20 @@ module DhanHQ
|
|
|
141
141
|
end
|
|
142
142
|
rescue StandardError => e
|
|
143
143
|
DhanHQ.logger&.error("[DhanHQ::WS] crashed #{e.class} #{e.message}")
|
|
144
|
+
DhanHQ.logger&.error("[DhanHQ::WS] backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
144
145
|
failed = true
|
|
146
|
+
# Reset connection state on error
|
|
147
|
+
@ws = nil
|
|
148
|
+
@timer = nil
|
|
145
149
|
ensure
|
|
150
|
+
# Clean up EventMachine resources
|
|
151
|
+
begin
|
|
152
|
+
EM.cancel_timer(@timer) if @timer
|
|
153
|
+
@timer = nil
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
DhanHQ.logger&.debug("[DhanHQ::WS] cleanup error: #{e.message}")
|
|
156
|
+
end
|
|
157
|
+
|
|
146
158
|
break if @stop
|
|
147
159
|
|
|
148
160
|
if got_429
|
|
@@ -154,11 +166,13 @@ module DhanHQ
|
|
|
154
166
|
# exponential backoff with jitter
|
|
155
167
|
sleep_time = [backoff, MAX_BACKOFF].min
|
|
156
168
|
jitter = rand(0.2 * sleep_time)
|
|
157
|
-
DhanHQ.logger&.warn("[DhanHQ::WS] reconnecting in #{(sleep_time + jitter).round(1)}s")
|
|
169
|
+
DhanHQ.logger&.warn("[DhanHQ::WS] reconnecting in #{(sleep_time + jitter).round(1)}s (backoff: #{backoff.round(1)}s)")
|
|
158
170
|
sleep(sleep_time + jitter)
|
|
159
171
|
backoff *= 2.0
|
|
160
172
|
else
|
|
161
|
-
|
|
173
|
+
# Reset backoff only after a clean session end (normal close with code 1000)
|
|
174
|
+
backoff = 2.0
|
|
175
|
+
DhanHQ.logger&.debug("[DhanHQ::WS] backoff reset to 2.0s after clean session end")
|
|
162
176
|
end
|
|
163
177
|
end
|
|
164
178
|
end
|
|
@@ -80,7 +80,8 @@ module DhanHQ
|
|
|
80
80
|
# @param config [Configuration] DhanHQ configuration
|
|
81
81
|
# @return [String] WebSocket URL
|
|
82
82
|
def build_market_depth_url(config)
|
|
83
|
-
token = config.
|
|
83
|
+
token = config.resolved_access_token
|
|
84
|
+
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
84
85
|
cid = config.client_id or raise "DhanHQ.client_id not set"
|
|
85
86
|
depth_level = config.market_depth_level || 20 # Default to 20 level depth
|
|
86
87
|
|
|
@@ -17,9 +17,9 @@ module DhanHQ
|
|
|
17
17
|
# @param options [Hash] Connection options
|
|
18
18
|
# @param block [Proc] Callback for depth updates
|
|
19
19
|
# @return [Client] WebSocket client instance
|
|
20
|
-
def connect(symbols: [],
|
|
21
|
-
client = Client.new(symbols: symbols, **
|
|
22
|
-
client.on(:depth_update, &
|
|
20
|
+
def connect(symbols: [], **, &)
|
|
21
|
+
client = Client.new(symbols: symbols, **)
|
|
22
|
+
client.on(:depth_update, &) if block_given?
|
|
23
23
|
client.start
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -28,8 +28,8 @@ module DhanHQ
|
|
|
28
28
|
# @param symbols [Array<String>] Symbols to subscribe to
|
|
29
29
|
# @param options [Hash] Connection options
|
|
30
30
|
# @return [Client] New client instance
|
|
31
|
-
def client(symbols: [], **
|
|
32
|
-
Client.new(symbols: symbols, **
|
|
31
|
+
def client(symbols: [], **)
|
|
32
|
+
Client.new(symbols: symbols, **)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
##
|
|
@@ -38,8 +38,8 @@ module DhanHQ
|
|
|
38
38
|
# @param handlers [Hash] Event handlers
|
|
39
39
|
# @param options [Hash] Connection options
|
|
40
40
|
# @return [Client] Started client instance
|
|
41
|
-
def connect_with_handlers(symbols: [], handlers: {}, **
|
|
42
|
-
client = Client.new(symbols: symbols, **
|
|
41
|
+
def connect_with_handlers(symbols: [], handlers: {}, **)
|
|
42
|
+
client = Client.new(symbols: symbols, **).start
|
|
43
43
|
|
|
44
44
|
handlers.each do |event, handler|
|
|
45
45
|
client.on(event, &handler)
|
|
@@ -54,8 +54,8 @@ module DhanHQ
|
|
|
54
54
|
# @param options [Hash] Connection options
|
|
55
55
|
# @param block [Proc] Callback for depth updates
|
|
56
56
|
# @return [Client] Started client instance
|
|
57
|
-
def subscribe(symbols:,
|
|
58
|
-
connect(symbols: symbols,
|
|
57
|
+
def subscribe(symbols:, **, &)
|
|
58
|
+
connect(symbols: symbols, **, &)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
##
|
|
@@ -64,9 +64,9 @@ module DhanHQ
|
|
|
64
64
|
# @param options [Hash] Connection options
|
|
65
65
|
# @param block [Proc] Callback for snapshot data
|
|
66
66
|
# @return [Client] Started client instance
|
|
67
|
-
def snapshot(symbols:,
|
|
68
|
-
client = Client.new(symbols: symbols, **
|
|
69
|
-
client.on(:depth_snapshot, &
|
|
67
|
+
def snapshot(symbols:, **, &)
|
|
68
|
+
client = Client.new(symbols: symbols, **)
|
|
69
|
+
client.on(:depth_snapshot, &) if block_given?
|
|
70
70
|
client.start
|
|
71
71
|
end
|
|
72
72
|
end
|