DhanHQ 2.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +20 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +555 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/README1.md +521 -0
- data/Rakefile +12 -0
- data/TAGS +10 -0
- data/TODO-1.md +14 -0
- data/TODO.md +127 -0
- data/app/services/live/order_update_guard_support.rb +75 -0
- data/app/services/live/order_update_hub.rb +76 -0
- data/app/services/live/order_update_persistence_support.rb +68 -0
- data/config/initializers/order_update_hub.rb +16 -0
- data/diagram.html +184 -0
- data/diagram.md +34 -0
- data/docs/rails_integration.md +304 -0
- data/exe/DhanHQ +4 -0
- data/lib/DhanHQ/client.rb +116 -0
- data/lib/DhanHQ/config.rb +32 -0
- data/lib/DhanHQ/configuration.rb +72 -0
- data/lib/DhanHQ/constants.rb +170 -0
- data/lib/DhanHQ/contracts/base_contract.rb +15 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
- data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
- data/lib/DhanHQ/contracts/order_contract.rb +102 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
- data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
- data/lib/DhanHQ/core/base_api.rb +105 -0
- data/lib/DhanHQ/core/base_model.rb +266 -0
- data/lib/DhanHQ/core/base_resource.rb +50 -0
- data/lib/DhanHQ/core/error_handler.rb +19 -0
- data/lib/DhanHQ/error_object.rb +49 -0
- data/lib/DhanHQ/errors.rb +45 -0
- data/lib/DhanHQ/helpers/api_helper.rb +17 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
- data/lib/DhanHQ/helpers/model_helper.rb +7 -0
- data/lib/DhanHQ/helpers/request_helper.rb +69 -0
- data/lib/DhanHQ/helpers/response_helper.rb +98 -0
- data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
- data/lib/DhanHQ/json_loader.rb +23 -0
- data/lib/DhanHQ/models/edis.rb +58 -0
- data/lib/DhanHQ/models/forever_order.rb +85 -0
- data/lib/DhanHQ/models/funds.rb +50 -0
- data/lib/DhanHQ/models/historical_data.rb +77 -0
- data/lib/DhanHQ/models/holding.rb +56 -0
- data/lib/DhanHQ/models/kill_switch.rb +49 -0
- data/lib/DhanHQ/models/ledger_entry.rb +60 -0
- data/lib/DhanHQ/models/margin.rb +54 -0
- data/lib/DhanHQ/models/market_feed.rb +41 -0
- data/lib/DhanHQ/models/option_chain.rb +79 -0
- data/lib/DhanHQ/models/order.rb +239 -0
- data/lib/DhanHQ/models/position.rb +60 -0
- data/lib/DhanHQ/models/profile.rb +44 -0
- data/lib/DhanHQ/models/super_order.rb +69 -0
- data/lib/DhanHQ/models/trade.rb +79 -0
- data/lib/DhanHQ/rate_limiter.rb +107 -0
- data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
- data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
- data/lib/DhanHQ/requests/orders/create.json +0 -0
- data/lib/DhanHQ/resources/edis.rb +44 -0
- data/lib/DhanHQ/resources/forever_orders.rb +53 -0
- data/lib/DhanHQ/resources/funds.rb +21 -0
- data/lib/DhanHQ/resources/historical_data.rb +34 -0
- data/lib/DhanHQ/resources/holdings.rb +21 -0
- data/lib/DhanHQ/resources/kill_switch.rb +21 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
- data/lib/DhanHQ/resources/market_feed.rb +56 -0
- data/lib/DhanHQ/resources/option_chain.rb +31 -0
- data/lib/DhanHQ/resources/orders.rb +70 -0
- data/lib/DhanHQ/resources/positions.rb +29 -0
- data/lib/DhanHQ/resources/profile.rb +25 -0
- data/lib/DhanHQ/resources/statements.rb +42 -0
- data/lib/DhanHQ/resources/super_orders.rb +46 -0
- data/lib/DhanHQ/resources/trades.rb +23 -0
- data/lib/DhanHQ/version.rb +6 -0
- data/lib/DhanHQ/ws/client.rb +182 -0
- data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
- data/lib/DhanHQ/ws/connection.rb +240 -0
- data/lib/DhanHQ/ws/decoder.rb +83 -0
- data/lib/DhanHQ/ws/errors.rb +0 -0
- data/lib/DhanHQ/ws/orders/client.rb +59 -0
- data/lib/DhanHQ/ws/orders/connection.rb +148 -0
- data/lib/DhanHQ/ws/orders.rb +13 -0
- data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
- data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
- data/lib/DhanHQ/ws/packets/header.rb +23 -0
- data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
- data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
- data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
- data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
- data/lib/DhanHQ/ws/registry.rb +46 -0
- data/lib/DhanHQ/ws/segments.rb +75 -0
- data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
- data/lib/DhanHQ/ws/sub_state.rb +59 -0
- data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
- data/lib/DhanHQ/ws.rb +37 -0
- data/lib/DhanHQ.rb +135 -0
- data/lib/ta/technical_analysis.rb +405 -0
- data/sig/DhanHQ.rbs +4 -0
- data/watchlist.csv +3 -0
- metadata +283 -0
data/diagram.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
```mermaid
|
3
|
+
flowchart LR
|
4
|
+
subgraph DhanHQ_Gem["dhanhq-client (gem)"]
|
5
|
+
A[WS::Client] --> B[WS::Connection]
|
6
|
+
B --> C{Dhan Feed<br/>wss://api-feed.dhan.co}
|
7
|
+
B --> D[WebsocketPacketParser + Packets]
|
8
|
+
D --> E[Decoder -> normalized tick]
|
9
|
+
A -->|:tick| E
|
10
|
+
end
|
11
|
+
|
12
|
+
subgraph Rails_API["Rails API app"]
|
13
|
+
E --> F[WSSupervisor]
|
14
|
+
F --> G[TickCache (latest LTP)]
|
15
|
+
F --> H[TickBus]
|
16
|
+
F --> I[CandleAggregator (5m)]
|
17
|
+
I -->|bar close| J[Strategy::SupertrendOptionLong]
|
18
|
+
F --> K[CloseStrikesManager (ATM ±1 manager)]
|
19
|
+
J --> L[ExecutionIntent + Signal (DB)]
|
20
|
+
J --> M[Execution::DhanRouter]
|
21
|
+
M -->|try SuperOrder| N((Dhan Super Orders))
|
22
|
+
M -->|fallback| O[Market BUY + LocalTrailing]
|
23
|
+
O --> G
|
24
|
+
subgraph Backoffice
|
25
|
+
P[SyncLoop] --> Q[(Positions/Holdings DB)]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
C -.binary frames.-> D
|
30
|
+
K -->|subscribe CE/PE| A
|
31
|
+
M -->|REST| N
|
32
|
+
P -->|REST| Q
|
33
|
+
|
34
|
+
```
|
@@ -0,0 +1,304 @@
|
|
1
|
+
# Rails Integration Guide for DhanHQ
|
2
|
+
|
3
|
+
This guide demonstrates how to wire the `DhanHQ` Ruby client into a Rails
|
4
|
+
application so you can automate trading flows, fetch data, and stream market
|
5
|
+
updates via WebSockets. The examples assume Rails 7+, but the concepts apply to
|
6
|
+
older versions as well.
|
7
|
+
|
8
|
+
## 1. Install the gem
|
9
|
+
|
10
|
+
Add the gem to your Rails application's `Gemfile` and bundle:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# Gemfile
|
14
|
+
gem 'DhanHQ', git: 'https://github.com/shubhamtaywade82/dhanhq-client.git', branch: 'main'
|
15
|
+
```
|
16
|
+
|
17
|
+
```bash
|
18
|
+
bundle install
|
19
|
+
```
|
20
|
+
|
21
|
+
If you package the gem privately you can also point to a released version from
|
22
|
+
RubyGems.
|
23
|
+
|
24
|
+
## 2. Configure credentials & initializer
|
25
|
+
|
26
|
+
Store the Dhan client id and access token using Rails credentials or ENV
|
27
|
+
variables. These two keys are **required** for `DhanHQ.configure_with_env` to
|
28
|
+
boot successfully:
|
29
|
+
|
30
|
+
| Variable | Description |
|
31
|
+
| --- | --- |
|
32
|
+
| `CLIENT_ID` | Dhan trading client id for the account you want to trade with. |
|
33
|
+
| `ACCESS_TOKEN` | REST/WebSocket access token (regenerate via the Dhan console or APIs). |
|
34
|
+
|
35
|
+
```bash
|
36
|
+
bin/rails credentials:edit
|
37
|
+
```
|
38
|
+
|
39
|
+
```yaml
|
40
|
+
dhanhq:
|
41
|
+
client_id: "1001234567"
|
42
|
+
access_token: "eyJhbGciOi..."
|
43
|
+
log_level: "info" # optional
|
44
|
+
base_url: "https://api.dhan.co/v2" # optional
|
45
|
+
ws_order_url: "wss://api-order-update.dhan.co" # optional
|
46
|
+
ws_user_type: "SELF" # optional (SELF or PARTNER)
|
47
|
+
partner_id: "your-partner-id" # optional when ws_user_type: PARTNER
|
48
|
+
partner_secret: "your-partner-secret" # optional when ws_user_type: PARTNER
|
49
|
+
```
|
50
|
+
|
51
|
+
Create an initializer so your app boots with the correct configuration via
|
52
|
+
environment variables (Rails credentials can be copied into ENV on boot):
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# config/initializers/dhanhq.rb
|
56
|
+
require 'DhanHQ'
|
57
|
+
|
58
|
+
if (creds = Rails.application.credentials.dig(:dhanhq))
|
59
|
+
ENV['CLIENT_ID'] ||= creds[:client_id]
|
60
|
+
ENV['ACCESS_TOKEN'] ||= creds[:access_token]
|
61
|
+
ENV['DHAN_LOG_LEVEL'] ||= creds[:log_level]&.upcase
|
62
|
+
ENV['DHAN_BASE_URL'] ||= creds[:base_url]
|
63
|
+
ENV['DHAN_WS_ORDER_URL'] ||= creds[:ws_order_url]
|
64
|
+
ENV['DHAN_WS_USER_TYPE'] ||= creds[:ws_user_type]
|
65
|
+
ENV['DHAN_PARTNER_ID'] ||= creds[:partner_id]
|
66
|
+
ENV['DHAN_PARTNER_SECRET'] ||= creds[:partner_secret]
|
67
|
+
end
|
68
|
+
|
69
|
+
# fall back to traditional ENV variables when credentials are not defined
|
70
|
+
ENV['CLIENT_ID'] ||= ENV.fetch('DHAN_CLIENT_ID', nil)
|
71
|
+
ENV['ACCESS_TOKEN'] ||= ENV.fetch('DHAN_ACCESS_TOKEN', nil)
|
72
|
+
|
73
|
+
DhanHQ.configure_with_env
|
74
|
+
|
75
|
+
log_level = (ENV['DHAN_LOG_LEVEL'] || 'INFO').upcase
|
76
|
+
DhanHQ.logger.level = Logger.const_get(log_level)
|
77
|
+
```
|
78
|
+
|
79
|
+
**Optional configuration**
|
80
|
+
|
81
|
+
Populate any of the following keys when you need to override the gem defaults
|
82
|
+
or enable partner streaming flows:
|
83
|
+
|
84
|
+
| Variable | Purpose |
|
85
|
+
| --- | --- |
|
86
|
+
| `DHAN_LOG_LEVEL` | Change the logger level (`INFO` default). |
|
87
|
+
| `DHAN_BASE_URL` | Target a different REST API host. |
|
88
|
+
| `DHAN_WS_VERSION` | Pin WebSocket connections to a specific API version. |
|
89
|
+
| `DHAN_WS_ORDER_URL` | Override the order update WebSocket endpoint. |
|
90
|
+
| `DHAN_WS_USER_TYPE` | Switch between `SELF` and `PARTNER` WebSocket auth. |
|
91
|
+
| `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when `DHAN_WS_USER_TYPE=PARTNER`. |
|
92
|
+
|
93
|
+
Set the variables in ENV (or in credentials copied to ENV) **before** the
|
94
|
+
initializer calls `DhanHQ.configure_with_env`.
|
95
|
+
|
96
|
+
## 3. Build service objects for REST flows
|
97
|
+
|
98
|
+
Wrap trading actions in plain-old Ruby objects so controllers and jobs stay thin:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# app/services/dhan/orders/place_order.rb
|
102
|
+
module Dhan
|
103
|
+
module Orders
|
104
|
+
class PlaceOrder
|
105
|
+
def initialize(params)
|
106
|
+
@params = params
|
107
|
+
end
|
108
|
+
|
109
|
+
def call
|
110
|
+
order = DhanHQ::Models::Order.new(@params)
|
111
|
+
order.save
|
112
|
+
order
|
113
|
+
rescue DhanHQ::Error => e
|
114
|
+
Rails.logger.error("Dhan order failed: #{e.message}")
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
Use the service from controllers, background jobs, or scheduled tasks:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
class OrdersController < ApplicationController
|
126
|
+
def create
|
127
|
+
order = Dhan::Orders::PlaceOrder.new(order_params).call
|
128
|
+
render json: order.attributes
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def order_params
|
134
|
+
params.require(:order).permit(:transaction_type, :exchange_segment, :product_type,
|
135
|
+
:order_type, :validity, :security_id, :quantity,
|
136
|
+
:price, :trigger_price, :correlation_id)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
The gem exposes models for positions, holdings, trades, funds, option chains,
|
142
|
+
historical bars, etc. Instantiate them the same way (`Model.all`, `.find`,
|
143
|
+
`.where`, `#save`).
|
144
|
+
|
145
|
+
## 4. Centralise error handling
|
146
|
+
|
147
|
+
Wrap the gem's exceptions in a concern so Rails controllers and jobs return
|
148
|
+
consistent responses:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# app/controllers/concerns/handles_dhan_errors.rb
|
152
|
+
module HandlesDhanErrors
|
153
|
+
extend ActiveSupport::Concern
|
154
|
+
|
155
|
+
included do
|
156
|
+
rescue_from DhanHQ::Error, with: :render_dhan_error
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def render_dhan_error(error)
|
162
|
+
Rails.logger.warn("Dhan API error: #{error.message}")
|
163
|
+
render json: { error: error.message, details: error.details }, status: :unprocessable_entity
|
164
|
+
end
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
Include the concern in API controllers or base controllers as needed.
|
169
|
+
|
170
|
+
## 5. Consume market data via WebSockets
|
171
|
+
|
172
|
+
The gem ships with an EventMachine-based client that can run inside your Rails
|
173
|
+
processes. The simplest approach is to start a dedicated process (e.g. a
|
174
|
+
Sidekiq worker or a Rails runner) that keeps the connection alive and publishes
|
175
|
+
ticks through ActionCable, Redis, or a database.
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
# app/workers/dhan/market_feed_worker.rb
|
179
|
+
class Dhan::MarketFeedWorker
|
180
|
+
include Sidekiq::Worker
|
181
|
+
|
182
|
+
def perform(mode = :quote, securities = [])
|
183
|
+
client = DhanHQ::WS::Client.new(mode: mode.to_sym)
|
184
|
+
|
185
|
+
client.on(:open) { Rails.logger.info('Dhan WS connected') }
|
186
|
+
client.on(:close) { Rails.logger.warn('Dhan WS closed; worker will retry') }
|
187
|
+
client.on(:error) { |err| Rails.logger.error("Dhan WS error: #{err}") }
|
188
|
+
|
189
|
+
client.on(:tick) do |tick|
|
190
|
+
ActionCable.server.broadcast('market_feed', tick)
|
191
|
+
end
|
192
|
+
|
193
|
+
client.start
|
194
|
+
client.subscribe(securities) if securities.any?
|
195
|
+
client.wait! # blocks the worker thread while EventMachine runs
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
Schedule the worker from `sidekiq.yml`, a scheduler, or run on demand:
|
201
|
+
|
202
|
+
```bash
|
203
|
+
bundle exec sidekiq -q default
|
204
|
+
bundle exec sidekiq-client push '{"class":"Dhan::MarketFeedWorker","args":["quote",[["NSE_EQ","1333"]]]}'
|
205
|
+
```
|
206
|
+
|
207
|
+
Define an ActionCable channel so browsers receive updates in real time:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
# app/channels/market_feed_channel.rb
|
211
|
+
class MarketFeedChannel < ApplicationCable::Channel
|
212
|
+
def subscribed
|
213
|
+
stream_from 'market_feed'
|
214
|
+
end
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
## 6. Stream order updates
|
219
|
+
|
220
|
+
Use the order-update WebSocket endpoint (configure `ws_order_url` and
|
221
|
+
`ws_user_type`) and process callbacks similarly:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
# app/workers/dhan/order_updates_worker.rb
|
225
|
+
class Dhan::OrderUpdatesWorker
|
226
|
+
include Sidekiq::Worker
|
227
|
+
|
228
|
+
def perform
|
229
|
+
client = DhanHQ::WS::Client.new(kind: :order_updates)
|
230
|
+
|
231
|
+
client.on(:order_update) do |payload|
|
232
|
+
OrderStatusUpdater.call(payload)
|
233
|
+
end
|
234
|
+
|
235
|
+
client.on(:error) { |err| Rails.logger.error("Dhan order WS error: #{err}") }
|
236
|
+
|
237
|
+
client.start
|
238
|
+
client.wait!
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
Inside `OrderStatusUpdater` you can reconcile the payload with your local order
|
244
|
+
records, notify users via ActionCable or email, etc.
|
245
|
+
|
246
|
+
## 7. Schedule automation & backfills
|
247
|
+
|
248
|
+
Leverage ActiveJob, Sidekiq, or any scheduler (Whenever, Clockwork, Cron) to run
|
249
|
+
recurring jobs that pull data or enforce trading rules:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
# app/jobs/dhan/refresh_positions_job.rb
|
253
|
+
class Dhan::RefreshPositionsJob < ApplicationJob
|
254
|
+
queue_as :default
|
255
|
+
|
256
|
+
def perform
|
257
|
+
positions = DhanHQ::Models::Position.all
|
258
|
+
positions.each { |position| PositionSync.call(position) }
|
259
|
+
end
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
Trigger from cron using `whenever`:
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
# config/schedule.rb
|
267
|
+
every 5.minutes do
|
268
|
+
runner 'Dhan::RefreshPositionsJob.perform_later'
|
269
|
+
end
|
270
|
+
```
|
271
|
+
|
272
|
+
## 8. Testing helpers
|
273
|
+
|
274
|
+
For tests, stub HTTP requests using WebMock or VCR. The client delegates all
|
275
|
+
REST calls through Faraday, so you can match on URLs under
|
276
|
+
`https://api.dhan.co/v2`. For WebSockets, inject a fake transport by stubbing
|
277
|
+
`DhanHQ::WS::Connection`.
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
# spec/support/dhanhq.rb
|
281
|
+
RSpec.configure do |config|
|
282
|
+
config.before(:each, dhan: true) do
|
283
|
+
stub_request(:post, %r{https://api\.dhan\.co/v2/orders}).to_return(
|
284
|
+
status: 200,
|
285
|
+
body: { status: 'success', order_id: '123' }.to_json,
|
286
|
+
headers: { 'Content-Type' => 'application/json' }
|
287
|
+
)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
## 9. Deployment notes
|
293
|
+
|
294
|
+
- Run WebSocket consumers outside the web dynos/processes so Puma/Passenger
|
295
|
+
threads are not blocked.
|
296
|
+
- Ensure the `access_token` is refreshed before expiry; wire a cron job or
|
297
|
+
admin panel action that updates the stored token and restarts workers.
|
298
|
+
- Monitor the gem's logger output for `429` or `503` responses to adjust retry
|
299
|
+
logic.
|
300
|
+
|
301
|
+
## 10. Further reading
|
302
|
+
|
303
|
+
- [GUIDE.md](../GUIDE.md) — in-depth overview of the gem's models and APIs.
|
304
|
+
- [README.md](../README.md) — quick start, features, and WebSocket usage.
|
data/exe/DhanHQ
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "json"
|
5
|
+
require "active_support/core_ext/hash/indifferent_access"
|
6
|
+
require_relative "errors"
|
7
|
+
require_relative "rate_limiter"
|
8
|
+
|
9
|
+
module DhanHQ
|
10
|
+
# The `Client` class provides a wrapper for HTTP requests to interact with the DhanHQ API.
|
11
|
+
# Responsible for:
|
12
|
+
# - Establishing and managing the HTTP connection
|
13
|
+
# - Handling authentication and request headers
|
14
|
+
# - Sending raw HTTP requests (`GET`, `POST`, `PUT`, `DELETE`)
|
15
|
+
# - Parsing JSON responses into HashWithIndifferentAccess
|
16
|
+
# - Handling standard HTTP errors (400, 401, 403, etc.)
|
17
|
+
# - Implementing **Rate Limiting** to avoid hitting API limits.
|
18
|
+
#
|
19
|
+
# It supports `GET`, `POST`, `PUT`, and `DELETE` requests with JSON encoding/decoding.
|
20
|
+
# Credentials (`access_token`, `client_id`) are automatically added to each request.
|
21
|
+
#
|
22
|
+
# @see https://dhanhq.co/docs/v2/ DhanHQ API Documentation
|
23
|
+
class Client
|
24
|
+
include DhanHQ::RequestHelper
|
25
|
+
include DhanHQ::ResponseHelper
|
26
|
+
|
27
|
+
# The Faraday connection object used for HTTP requests.
|
28
|
+
#
|
29
|
+
# @return [Faraday::Connection] The connection instance used for API requests.
|
30
|
+
attr_reader :connection
|
31
|
+
|
32
|
+
# Initializes a new DhanHQ Client instance with a Faraday connection.
|
33
|
+
#
|
34
|
+
# @example Create a new client:
|
35
|
+
# client = DhanHQ::Client.new(api_type: :order_api)
|
36
|
+
#
|
37
|
+
# @param api_type [Symbol] Type of API (`:order_api`, `:data_api`, `:non_trading_api`)
|
38
|
+
# @return [DhanHQ::Client] A new client instance.
|
39
|
+
def initialize(api_type:)
|
40
|
+
DhanHQ.configure_with_env if ENV.fetch("CLIENT_ID", nil)
|
41
|
+
@rate_limiter = RateLimiter.new(api_type)
|
42
|
+
|
43
|
+
raise "RateLimiter initialization failed" unless @rate_limiter
|
44
|
+
|
45
|
+
@connection = Faraday.new(url: DhanHQ.configuration.base_url) do |conn|
|
46
|
+
conn.request :json, parser_options: { symbolize_names: true }
|
47
|
+
conn.response :json, content_type: /\bjson$/
|
48
|
+
conn.response :logger if ENV["DHAN_DEBUG"] == "true"
|
49
|
+
conn.adapter Faraday.default_adapter
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sends an HTTP request to the API.
|
54
|
+
#
|
55
|
+
# @param method [Symbol] The HTTP method (`:get`, `:post`, `:put`, `:delete`)
|
56
|
+
# @param path [String] The API endpoint path.
|
57
|
+
# @param payload [Hash] The request parameters or body.
|
58
|
+
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>] Parsed JSON response.
|
59
|
+
# @raise [DhanHQ::Error] If an HTTP error occurs.
|
60
|
+
def request(method, path, payload)
|
61
|
+
@rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
|
62
|
+
|
63
|
+
response = connection.send(method) do |req|
|
64
|
+
req.url path
|
65
|
+
req.headers.merge!(build_headers(path))
|
66
|
+
prepare_payload(req, payload, method)
|
67
|
+
end
|
68
|
+
|
69
|
+
handle_response(response)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Convenience wrapper for issuing a GET request.
|
73
|
+
#
|
74
|
+
# @param path [String] The API endpoint path.
|
75
|
+
# @param params [Hash] Query parameters for the request.
|
76
|
+
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>]
|
77
|
+
# Parsed JSON response.
|
78
|
+
# @see #request
|
79
|
+
def get(path, params = {})
|
80
|
+
request(:get, path, params)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Convenience wrapper for issuing a POST request.
|
84
|
+
#
|
85
|
+
# @param path [String] The API endpoint path.
|
86
|
+
# @param params [Hash] JSON payload for the request.
|
87
|
+
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>]
|
88
|
+
# Parsed JSON response.
|
89
|
+
# @see #request
|
90
|
+
def post(path, params = {})
|
91
|
+
request(:post, path, params)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Convenience wrapper for issuing a PUT request.
|
95
|
+
#
|
96
|
+
# @param path [String] The API endpoint path.
|
97
|
+
# @param params [Hash] JSON payload for the request.
|
98
|
+
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>]
|
99
|
+
# Parsed JSON response.
|
100
|
+
# @see #request
|
101
|
+
def put(path, params = {})
|
102
|
+
request(:put, path, params)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Convenience wrapper for issuing a DELETE request.
|
106
|
+
#
|
107
|
+
# @param path [String] The API endpoint path.
|
108
|
+
# @param params [Hash] Optional request payload (rare for DELETE).
|
109
|
+
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>]
|
110
|
+
# Parsed JSON response.
|
111
|
+
# @see #request
|
112
|
+
def delete(path, params = {})
|
113
|
+
request(:delete, path, params)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module DhanHQ
|
6
|
+
class << self
|
7
|
+
# keep whatever you already have; add these if missing:
|
8
|
+
attr_accessor :client_id, :access_token, :base_url, :ws_version
|
9
|
+
|
10
|
+
# default logger so calls like DhanHQ.logger&.info never explode
|
11
|
+
def logger
|
12
|
+
@logger ||= Logger.new($stdout, level: Logger::INFO)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_writer :logger
|
16
|
+
|
17
|
+
# same API style as your README
|
18
|
+
def configure
|
19
|
+
yield self
|
20
|
+
# ensure a logger is present even if user didn’t set one
|
21
|
+
self.logger ||= Logger.new($stdout, level: Logger::INFO)
|
22
|
+
end
|
23
|
+
|
24
|
+
# if you support env bootstrap
|
25
|
+
def configure_with_env
|
26
|
+
self.client_id = ENV.fetch("CLIENT_ID", nil)
|
27
|
+
self.access_token = ENV.fetch("ACCESS_TOKEN", nil)
|
28
|
+
self.base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
|
29
|
+
self.ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# The `Configuration` class manages API credentials and settings.
|
5
|
+
#
|
6
|
+
# Use this class to set the required `access_token` and `client_id`, as well as optional
|
7
|
+
# settings such as the base URL and CSV URLs.
|
8
|
+
#
|
9
|
+
# @see https://dhanhq.co/docs/v2/ DhanHQ API Documentation
|
10
|
+
class Configuration
|
11
|
+
# Default REST API host used when the base URL is not overridden.
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
BASE_URL = "https://api.dhan.co/v2"
|
15
|
+
# The client ID for API authentication.
|
16
|
+
# @return [String, nil] The client ID or `nil` if not set.
|
17
|
+
attr_accessor :client_id
|
18
|
+
|
19
|
+
# The access token for API authentication.
|
20
|
+
# @return [String, nil] The access token or `nil` if not set.
|
21
|
+
attr_accessor :access_token
|
22
|
+
|
23
|
+
# The base URL for API requests.
|
24
|
+
# @return [String] The base URL for the DhanHQ API.
|
25
|
+
attr_accessor :base_url
|
26
|
+
|
27
|
+
# URL for the compact CSV format of instruments.
|
28
|
+
# @return [String] URL for compact CSV.
|
29
|
+
attr_accessor :compact_csv_url
|
30
|
+
|
31
|
+
# URL for the detailed CSV format of instruments.
|
32
|
+
# @return [String] URL for detailed CSV.
|
33
|
+
attr_accessor :detailed_csv_url
|
34
|
+
|
35
|
+
# Websocket API version.
|
36
|
+
# @return [Integer]
|
37
|
+
attr_accessor :ws_version
|
38
|
+
|
39
|
+
# Websocket order updates endpoint.
|
40
|
+
# @return [String]
|
41
|
+
attr_accessor :ws_order_url
|
42
|
+
|
43
|
+
# Websocket user type for order updates.
|
44
|
+
# @return [String] "SELF" or "PARTNER".
|
45
|
+
attr_accessor :ws_user_type
|
46
|
+
|
47
|
+
# Partner ID for order updates when `ws_user_type` is "PARTNER".
|
48
|
+
# @return [String, nil]
|
49
|
+
attr_accessor :partner_id
|
50
|
+
|
51
|
+
# Partner secret for order updates when `ws_user_type` is "PARTNER".
|
52
|
+
# @return [String, nil]
|
53
|
+
attr_accessor :partner_secret
|
54
|
+
|
55
|
+
# Initializes a new configuration instance with default values.
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# config = DhanHQ::Configuration.new
|
59
|
+
# config.client_id = "your_client_id"
|
60
|
+
# config.access_token = "your_access_token"
|
61
|
+
def initialize
|
62
|
+
@client_id = ENV.fetch("CLIENT_ID", nil)
|
63
|
+
@access_token = ENV.fetch("ACCESS_TOKEN", nil)
|
64
|
+
@base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
|
65
|
+
@ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
66
|
+
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
|
67
|
+
@ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
|
68
|
+
@partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
|
69
|
+
@partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", nil)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|