DhanHQ 2.3.0 → 2.5.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/CODE_REVIEW_ISSUES.md +2 -2
  4. data/GUIDE.md +2 -2
  5. data/README.md +194 -741
  6. data/REVIEW_SUMMARY.md +2 -2
  7. data/{README1.md → docs/ARCHIVE_README.md} +4 -4
  8. data/docs/AUTHENTICATION.md +116 -2
  9. data/docs/CONFIGURATION.md +109 -0
  10. data/docs/SUPER_ORDERS.md +284 -0
  11. data/docs/TESTING_GUIDE.md +8 -8
  12. data/docs/TROUBLESHOOTING.md +117 -0
  13. data/docs/WEBSOCKET_PROTOCOL.md +154 -0
  14. data/docs/live_order_updates.md +2 -2
  15. data/docs/rails_integration.md +7 -7
  16. data/docs/standalone_ruby_websocket_integration.md +24 -24
  17. data/docs/technical_analysis.md +1 -1
  18. data/docs/websocket_integration.md +4 -4
  19. data/examples/comprehensive_websocket_examples.rb +2 -2
  20. data/examples/instrument_finder_test.rb +2 -2
  21. data/examples/market_depth_example.rb +2 -2
  22. data/examples/market_feed_example.rb +2 -2
  23. data/examples/order_update_example.rb +2 -2
  24. data/examples/trading_fields_example.rb +2 -2
  25. data/lib/DhanHQ/auth/token_generator.rb +33 -0
  26. data/lib/DhanHQ/auth/token_manager.rb +88 -0
  27. data/lib/DhanHQ/auth/token_renewal.rb +25 -0
  28. data/lib/DhanHQ/auth.rb +91 -31
  29. data/lib/DhanHQ/client.rb +42 -2
  30. data/lib/DhanHQ/configuration.rb +2 -2
  31. data/lib/DhanHQ/contracts/order_contract.rb +0 -23
  32. data/lib/DhanHQ/contracts/trade_by_order_id_contract.rb +12 -0
  33. data/lib/DhanHQ/contracts/trade_contract.rb +0 -65
  34. data/lib/DhanHQ/contracts/trade_history_contract.rb +52 -0
  35. data/lib/DhanHQ/core/auth_api.rb +21 -0
  36. data/lib/DhanHQ/helpers/request_helper.rb +1 -1
  37. data/lib/DhanHQ/models/alert_order.rb +22 -0
  38. data/lib/DhanHQ/models/edis.rb +110 -0
  39. data/lib/DhanHQ/models/kill_switch.rb +22 -0
  40. data/lib/DhanHQ/models/margin.rb +49 -0
  41. data/lib/DhanHQ/models/pnl_exit.rb +130 -0
  42. data/lib/DhanHQ/models/position.rb +22 -0
  43. data/lib/DhanHQ/models/postback.rb +123 -0
  44. data/lib/DhanHQ/models/token_response.rb +88 -0
  45. data/lib/DhanHQ/resources/kill_switch.rb +8 -0
  46. data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
  47. data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
  48. data/lib/DhanHQ/resources/positions.rb +8 -0
  49. data/lib/DhanHQ/version.rb +1 -1
  50. data/lib/dhan_hq.rb +31 -81
  51. metadata +46 -4
  52. data/lib/DhanHQ/config.rb +0 -33
@@ -0,0 +1,117 @@
1
+ # Troubleshooting
2
+
3
+ Common issues and solutions when working with the DhanHQ Ruby client.
4
+
5
+ ---
6
+
7
+ ## 429: Unexpected Response Code
8
+
9
+ **Symptom:** WebSocket connection fails with a 429 status.
10
+
11
+ **Cause:** Too many connections opened in quick succession, or exceeding the per-user WebSocket connection limit (5 per user).
12
+
13
+ **Solution:**
14
+ - The client automatically cools off for **60 seconds** and retries with exponential backoff.
15
+ - Prefer `ws.disconnect!` before reconnecting to cleanly release server-side resources.
16
+ - Call `DhanHQ::WS.disconnect_all_local!` to kill any straggler connections.
17
+ - Avoid rapid connect/disconnect loops — the client handles backoff internally.
18
+
19
+ ```ruby
20
+ # Kill all local WebSocket connections
21
+ DhanHQ::WS.disconnect_all_local!
22
+
23
+ # Wait before reconnecting
24
+ sleep(2)
25
+
26
+ # Reconnect
27
+ client = DhanHQ::WS.connect(mode: :ticker) { |tick| puts tick[:ltp] }
28
+ ```
29
+
30
+ ---
31
+
32
+ ## No Ticks After Reconnect
33
+
34
+ **Symptom:** WebSocket reconnects successfully but no market data arrives.
35
+
36
+ **Cause:** Subscriptions were not restored after the connection dropped.
37
+
38
+ **Solution:**
39
+ - The client **automatically resends** the current subscription snapshot on reconnect — this should work transparently.
40
+ - If you're managing connections manually, ensure you re-subscribe after a clean start.
41
+ - Check that your instruments are valid and the market is open.
42
+
43
+ ---
44
+
45
+ ## Binary Parse Errors
46
+
47
+ **Symptom:** Errors in logs related to binary frame parsing.
48
+
49
+ **Cause:** Malformed or unexpected binary frames from the server.
50
+
51
+ **Solution:**
52
+ - The client safely drops malformed frames and keeps the event loop alive.
53
+ - Run with `DHAN_LOG_LEVEL=DEBUG` to inspect raw frames:
54
+
55
+ ```bash
56
+ export DHAN_LOG_LEVEL=DEBUG
57
+ ```
58
+
59
+ ```ruby
60
+ DhanHQ.logger.level = Logger::DEBUG
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Authentication Errors
66
+
67
+ | Error Class | Meaning |
68
+ | ------------------------------------ | ---------------------------------------------------------- |
69
+ | `DhanHQ::AuthenticationError` | Token could not be resolved (missing config, nil provider) |
70
+ | `DhanHQ::InvalidAuthenticationError` | API returned 401 or error code DH-901 |
71
+ | `DhanHQ::TokenExpiredError` | API returned error code 807 (token expired) |
72
+ | `DhanHQ::InvalidTokenError` | API returned error code 809 (invalid token) |
73
+
74
+ **Solutions:**
75
+ - Verify `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN` are set correctly.
76
+ - If using `access_token_provider`, ensure it returns a non-nil string.
77
+ - For 401 retries: the client retries **once** with a fresh token when `access_token_provider` is configured.
78
+ - See [AUTHENTICATION.md](AUTHENTICATION.md) for detailed token lifecycle handling.
79
+
80
+ ---
81
+
82
+ ## Connection Timeouts
83
+
84
+ **Symptom:** REST API calls hang or fail with timeout errors.
85
+
86
+ **Solution:** Adjust timeout settings via environment variables:
87
+
88
+ ```dotenv
89
+ DHAN_CONNECT_TIMEOUT=15 # default: 10 seconds
90
+ DHAN_READ_TIMEOUT=60 # default: 30 seconds
91
+ DHAN_WRITE_TIMEOUT=60 # default: 30 seconds
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Debug Logging
97
+
98
+ Enable full debug output to diagnose any issue:
99
+
100
+ ```ruby
101
+ DhanHQ.logger.level = Logger::DEBUG
102
+ ```
103
+
104
+ Or via environment:
105
+
106
+ ```bash
107
+ export DHAN_LOG_LEVEL=DEBUG
108
+ ```
109
+
110
+ This logs HTTP requests/responses, WebSocket frames, and internal state transitions.
111
+
112
+ ---
113
+
114
+ ## Getting Help
115
+
116
+ - [DhanHQ GitHub Issues](https://github.com/shubhamtaywade82/dhanhq-client/issues)
117
+ - [Dhan API Documentation](https://dhanhq.co/docs/v2/)
@@ -0,0 +1,154 @@
1
+ # WebSocket Protocol Reference
2
+
3
+ Low-level protocol details for the DhanHQ WebSocket market feed. For high-level usage, see the [WebSocket Integration Guide](websocket_integration.md).
4
+
5
+ ---
6
+
7
+ ## Subscription Modes
8
+
9
+ | Mode | What you get | Best for |
10
+ | --------- | ----------------------------------------- | ------------------------------- |
11
+ | `:ticker` | LTP + LTT | Lightweight price monitoring |
12
+ | `:quote` | LTP + LTT + OHLCV + totals | Most trading strategies |
13
+ | `:full` | Quote + OI + best-5 depth (bid/ask) | Order book analysis, depth-based strategies |
14
+
15
+ ---
16
+
17
+ ## Request Codes
18
+
19
+ Per Dhan documentation:
20
+
21
+ | Action | Ticker | Quote | Full |
22
+ | ------------ | ------ | ----- | ---- |
23
+ | Subscribe | 15 | 17 | 21 |
24
+ | Unsubscribe | 16 | 18 | 22 |
25
+ | Disconnect | 12 | 12 | 12 |
26
+
27
+ ---
28
+
29
+ ## Packet Parsing
30
+
31
+ ### Response Header (8 bytes)
32
+
33
+ | Field | Size | Encoding | Description |
34
+ | -------------------- | ------ | -------- | ----------------------------- |
35
+ | `feed_response_code` | 1 byte | u8, BE | Identifies the packet type |
36
+ | `message_length` | 2 bytes| u16, BE | Total message length in bytes |
37
+ | `exchange_segment` | 1 byte | u8, BE | Exchange segment identifier |
38
+ | `security_id` | 4 bytes| i32, LE | Security identifier |
39
+
40
+ ### Packet Types
41
+
42
+ | Code | Type | Fields |
43
+ | ---- | ------------- | ------------------------------------------------------------- |
44
+ | 1 | Index | Surfaced as raw/misc unless documented |
45
+ | 2 | Ticker | `ltp`, `ltt` |
46
+ | 4 | Quote | `ltp`, `ltt`, `atp`, `volume`, totals, `day_*` |
47
+ | 5 | OI | `open_interest` |
48
+ | 6 | Prev Close | `prev_close`, `oi_prev` |
49
+ | 7 | Market Status | Raw/misc unless documented |
50
+ | 8 | Full | Quote fields + `open_interest` + 5× depth (bid/ask) |
51
+ | 50 | Disconnect | Reason code |
52
+
53
+ ---
54
+
55
+ ## Normalized Tick Schema
56
+
57
+ All ticks are delivered as a Ruby Hash with consistent keys:
58
+
59
+ ```ruby
60
+ {
61
+ kind: :quote, # :ticker | :quote | :full | :oi | :prev_close | :misc
62
+ segment: "NSE_FNO", # string enum
63
+ security_id: "12345",
64
+ ltp: 101.5,
65
+ ts: 1723791300, # LTT epoch (sec) if present
66
+ vol: 123456, # quote/full only
67
+ atp: 100.9, # quote/full only
68
+ day_open: 100.1,
69
+ day_high: 102.4,
70
+ day_low: 99.5,
71
+ day_close: nil,
72
+ oi: 987654, # full or OI packet
73
+ bid: 101.45, # from depth (mode :full)
74
+ ask: 101.55 # from depth (mode :full)
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Connection Limits & Behavior
81
+
82
+ ### Limits
83
+
84
+ - **100 instruments** per subscribe/unsubscribe frame (auto-chunked by the client)
85
+ - **5 WebSocket connections** per user (per Dhan)
86
+
87
+ ### Backoff & 429 Cool-Off
88
+
89
+ - Exponential backoff with jitter on connection failure
90
+ - Handshake **429** triggers a **60-second cool-off** before retry
91
+ - The client handles this automatically — avoid manual rapid reconnect loops
92
+
93
+ ### Reconnect & Resubscribe
94
+
95
+ - On reconnect, the client resends the **current subscription snapshot** (idempotent)
96
+ - No manual re-subscribe needed after automatic reconnection
97
+
98
+ ### Graceful Shutdown
99
+
100
+ - `ws.disconnect!` — sends broker disconnect code 12, prevents reconnects
101
+ - `ws.stop` — hard stop (no broker message, just closes and halts loop)
102
+ - `DhanHQ::WS.disconnect_all_local!` — kills all registered WS clients
103
+ - An `at_exit` hook stops all registered clients to avoid leaked sockets
104
+
105
+ ---
106
+
107
+ ## Exchange Segment Enums
108
+
109
+ Use these string enums in WebSocket `subscribe_*` calls and REST parameters:
110
+
111
+ | Enum | Exchange | Segment |
112
+ | -------------- | -------- | ----------------- |
113
+ | `IDX_I` | Index | Index Value |
114
+ | `NSE_EQ` | NSE | Equity Cash |
115
+ | `NSE_FNO` | NSE | Futures & Options |
116
+ | `NSE_CURRENCY` | NSE | Currency |
117
+ | `BSE_EQ` | BSE | Equity Cash |
118
+ | `BSE_FNO` | BSE | Futures & Options |
119
+ | `BSE_CURRENCY` | BSE | Currency |
120
+ | `MCX_COMM` | MCX | Commodity |
121
+
122
+ ---
123
+
124
+ ## Tick Access Patterns
125
+
126
+ ### Direct Handler
127
+
128
+ ```ruby
129
+ ws.on(:tick) { |t| do_something_fast(t) } # avoid heavy work here
130
+ ```
131
+
132
+ ### Shared TickCache (Recommended)
133
+
134
+ ```ruby
135
+ # app/services/live/tick_cache.rb
136
+ class TickCache
137
+ MAP = Concurrent::Map.new
138
+ def self.put(t) = MAP["#{t[:segment]}:#{t[:security_id]}"] = t
139
+ def self.get(seg, sid) = MAP["#{seg}:#{sid}"]
140
+ def self.ltp(seg, sid) = get(seg, sid)&.dig(:ltp)
141
+ end
142
+
143
+ ws.on(:tick) { |t| TickCache.put(t) }
144
+ ltp = TickCache.ltp("NSE_FNO", "12345")
145
+ ```
146
+
147
+ ### Filtered Callback
148
+
149
+ ```ruby
150
+ def on_tick_for(ws, segment:, security_id:, &blk)
151
+ key = "#{segment}:#{security_id}"
152
+ ws.on(:tick) { |t| blk.call(t) if "#{t[:segment]}:#{t[:security_id]}" == key }
153
+ end
154
+ ```
@@ -194,8 +194,8 @@ client.start
194
194
  ### Environment Variables
195
195
  ```bash
196
196
  # Required
197
- CLIENT_ID=your_client_id
198
- ACCESS_TOKEN=your_access_token
197
+ DHAN_CLIENT_ID=your_client_id
198
+ DHAN_ACCESS_TOKEN=your_access_token
199
199
 
200
200
  # Optional WebSocket settings
201
201
  DHAN_WS_ORDER_URL=wss://api-order-update.dhan.co
@@ -29,8 +29,8 @@ boot successfully:
29
29
 
30
30
  | Variable | Description |
31
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). |
32
+ | `DHAN_CLIENT_ID` | Dhan trading client id for the account you want to trade with. |
33
+ | `DHAN_ACCESS_TOKEN` | REST/WebSocket access token (regenerate via the Dhan console or APIs). |
34
34
 
35
35
  ```bash
36
36
  bin/rails credentials:edit
@@ -61,8 +61,8 @@ environment variables (Rails credentials can be copied into ENV on boot):
61
61
  require 'dhan_hq'
62
62
 
63
63
  if (creds = Rails.application.credentials.dig(:dhanhq))
64
- ENV['CLIENT_ID'] ||= creds[:client_id]
65
- ENV['ACCESS_TOKEN'] ||= creds[:access_token]
64
+ ENV['DHAN_CLIENT_ID'] ||= creds[:client_id]
65
+ ENV['DHAN_ACCESS_TOKEN'] ||= creds[:access_token]
66
66
  ENV['DHAN_LOG_LEVEL'] ||= creds[:log_level]&.upcase
67
67
  ENV['DHAN_BASE_URL'] ||= creds[:base_url]
68
68
  ENV['DHAN_WS_ORDER_URL'] ||= creds[:ws_order_url]
@@ -77,8 +77,8 @@ if (creds = Rails.application.credentials.dig(:dhanhq))
77
77
  end
78
78
 
79
79
  # fall back to traditional ENV variables when credentials are not defined
80
- ENV['CLIENT_ID'] ||= ENV.fetch('DHAN_CLIENT_ID', nil)
81
- ENV['ACCESS_TOKEN'] ||= ENV.fetch('DHAN_ACCESS_TOKEN', nil)
80
+ ENV['DHAN_CLIENT_ID'] ||= ENV.fetch('DHAN_CLIENT_ID', nil)
81
+ ENV['DHAN_ACCESS_TOKEN'] ||= ENV.fetch('DHAN_ACCESS_TOKEN', nil)
82
82
 
83
83
  DhanHQ.configure_with_env
84
84
 
@@ -108,7 +108,7 @@ initializer calls `DhanHQ.configure_with_env`.
108
108
  For token rotation without restarting the app (e.g. token stored in DB or refreshed via OAuth), use `access_token_provider` so the token is resolved at request time:
109
109
 
110
110
  ```ruby
111
- # config/initializers/dhanhq.rb (alternative to static ACCESS_TOKEN)
111
+ # config/initializers/dhanhq.rb (alternative to static DHAN_ACCESS_TOKEN)
112
112
  DhanHQ.configure do |config|
113
113
  config.client_id = ENV["DHAN_CLIENT_ID"] || Rails.application.credentials.dig(:dhanhq, :client_id)
114
114
  config.access_token_provider = lambda do
@@ -31,8 +31,8 @@ require 'dhan_hq'
31
31
 
32
32
  # Configure DhanHQ
33
33
  DhanHQ.configure do |config|
34
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
35
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
34
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
35
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
36
36
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
37
37
  end
38
38
 
@@ -59,8 +59,8 @@ market_client.stop
59
59
 
60
60
  ```bash
61
61
  # Set environment variables
62
- export CLIENT_ID="your_client_id"
63
- export ACCESS_TOKEN="your_access_token"
62
+ export DHAN_CLIENT_ID="your_client_id"
63
+ export DHAN_ACCESS_TOKEN="your_access_token"
64
64
 
65
65
  # Run the script
66
66
  ruby market_feed_script.rb
@@ -74,8 +74,8 @@ For dynamic token at request time use `config.access_token_provider`. For web-ge
74
74
 
75
75
  ```bash
76
76
  # Required
77
- export CLIENT_ID="your_client_id"
78
- export ACCESS_TOKEN="your_access_token"
77
+ export DHAN_CLIENT_ID="your_client_id"
78
+ export DHAN_ACCESS_TOKEN="your_access_token"
79
79
 
80
80
  # Optional
81
81
  export DHAN_WS_USER_TYPE="SELF" # or "PARTNER"
@@ -132,8 +132,8 @@ require 'json'
132
132
 
133
133
  # Configure DhanHQ
134
134
  DhanHQ.configure do |config|
135
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
136
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
135
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
136
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
137
137
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
138
138
  end
139
139
 
@@ -264,8 +264,8 @@ require 'json'
264
264
 
265
265
  # Configure DhanHQ
266
266
  DhanHQ.configure do |config|
267
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
268
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
267
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
268
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
269
269
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
270
270
  end
271
271
 
@@ -394,8 +394,8 @@ require 'json'
394
394
 
395
395
  # Configure DhanHQ
396
396
  DhanHQ.configure do |config|
397
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
398
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
397
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
398
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
399
399
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
400
400
  end
401
401
 
@@ -542,8 +542,8 @@ WorkingDirectory=/opt/dhanhq-market-feed
542
542
  ExecStart=/usr/bin/ruby market_feed_daemon.rb
543
543
  Restart=always
544
544
  RestartSec=10
545
- Environment=CLIENT_ID=your_client_id
546
- Environment=ACCESS_TOKEN=your_access_token
545
+ Environment=DHAN_CLIENT_ID=your_client_id
546
+ Environment=DHAN_ACCESS_TOKEN=your_access_token
547
547
  Environment=DHAN_WS_USER_TYPE=SELF
548
548
 
549
549
  [Install]
@@ -562,8 +562,8 @@ require 'fileutils'
562
562
 
563
563
  # Configure DhanHQ
564
564
  DhanHQ.configure do |config|
565
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
566
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
565
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
566
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
567
567
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
568
568
  end
569
569
 
@@ -779,8 +779,8 @@ require 'json'
779
779
 
780
780
  # Configure DhanHQ
781
781
  DhanHQ.configure do |config|
782
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
783
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
782
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
783
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
784
784
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
785
785
  end
786
786
 
@@ -1061,8 +1061,8 @@ require 'json'
1061
1061
 
1062
1062
  # Configure DhanHQ
1063
1063
  DhanHQ.configure do |config|
1064
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
1065
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
1064
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
1065
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
1066
1066
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
1067
1067
  end
1068
1068
 
@@ -1264,8 +1264,8 @@ end
1264
1264
 
1265
1265
  # Configure DhanHQ
1266
1266
  DhanHQ.configure do |config|
1267
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
1268
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
1267
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
1268
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
1269
1269
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
1270
1270
  config.logger = logger
1271
1271
  end
@@ -1383,8 +1383,8 @@ require 'json'
1383
1383
 
1384
1384
  # Configure DhanHQ
1385
1385
  DhanHQ.configure do |config|
1386
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
1387
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
1386
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
1387
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
1388
1388
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
1389
1389
  end
1390
1390
 
@@ -12,7 +12,7 @@ This guide explains how to use the technical analysis modules bundled with this
12
12
 
13
13
  ## Prerequisites
14
14
 
15
- - Environment variables set: `CLIENT_ID`, `ACCESS_TOKEN`
15
+ - Environment variables set: `DHAN_CLIENT_ID`, `DHAN_ACCESS_TOKEN`
16
16
  - Optional indicator gems:
17
17
  - `gem install ruby-technical-analysis technical-analysis`
18
18
 
@@ -28,8 +28,8 @@ require 'dhan_hq'
28
28
 
29
29
  # Configure DhanHQ
30
30
  DhanHQ.configure do |config|
31
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
32
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
31
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
32
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
33
33
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
34
34
  end
35
35
  # For dynamic token: use config.access_token_provider. For web-generated tokens, refresh with DhanHQ::Auth.renew_token. API key/Partner flows: implement in your app. See docs/AUTHENTICATION.md.
@@ -787,8 +787,8 @@ puts "Depth subscriptions: #{depth_client.subscriptions}"
787
787
  ```ruby
788
788
  # ✅ Good: Environment variables
789
789
  DhanHQ.configure do |config|
790
- config.client_id = ENV["CLIENT_ID"]
791
- config.access_token = ENV["ACCESS_TOKEN"]
790
+ config.client_id = ENV["DHAN_CLIENT_ID"]
791
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"]
792
792
  end
793
793
 
794
794
  # ❌ Bad: Hardcoded credentials
@@ -12,8 +12,8 @@ require "dhan_hq"
12
12
 
13
13
  # Configure DhanHQ
14
14
  DhanHQ.configure do |config|
15
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
16
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
15
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
16
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
17
17
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
18
18
  end
19
19
 
@@ -8,8 +8,8 @@ require "dhan_hq"
8
8
 
9
9
  # Configure DhanHQ
10
10
  DhanHQ.configure do |config|
11
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
12
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
11
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
12
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
13
13
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
14
14
  end
15
15
 
@@ -10,8 +10,8 @@ require "dhan_hq"
10
10
 
11
11
  # Configure DhanHQ
12
12
  DhanHQ.configure do |config|
13
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
14
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
13
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
14
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
15
15
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
16
16
  end
17
17
 
@@ -10,8 +10,8 @@ require "dhan_hq"
10
10
 
11
11
  # Configure DhanHQ
12
12
  DhanHQ.configure do |config|
13
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
14
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
13
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
14
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
15
15
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
16
16
  end
17
17
 
@@ -10,8 +10,8 @@ require "dhan_hq"
10
10
 
11
11
  # Configure DhanHQ
12
12
  DhanHQ.configure do |config|
13
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
14
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
13
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
14
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
15
15
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
16
16
  end
17
17
 
@@ -9,8 +9,8 @@ require "dhan_hq"
9
9
 
10
10
  # Configure DhanHQ
11
11
  DhanHQ.configure do |config|
12
- config.client_id = ENV["CLIENT_ID"] || "your_client_id"
13
- config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
12
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
13
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
14
14
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
15
15
  end
16
16
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Auth
5
+ # Backward-compatible wrapper for token generation.
6
+ # Delegates to module-level Auth.generate_access_token.
7
+ class TokenGenerator
8
+ def generate(dhan_client_id:, pin:, totp: nil, totp_secret: nil)
9
+ otp = resolve_totp(totp, totp_secret)
10
+
11
+ response = Auth.generate_access_token(
12
+ dhan_client_id: dhan_client_id,
13
+ pin: pin,
14
+ totp: otp
15
+ )
16
+
17
+ Models::TokenResponse.new(response)
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_totp(totp, secret)
23
+ totp = totp.to_s.strip
24
+ return totp unless totp.empty?
25
+
26
+ secret = secret.to_s.strip
27
+ raise ArgumentError, "Provide `totp` or `totp_secret` (e.g. ENV['DHAN_TOTP_SECRET'])" if secret.empty?
28
+
29
+ Auth.generate_totp(secret)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module DhanHQ
6
+ module Auth
7
+ # Manages automatic token lifecycle including generation, renewal, and validation.
8
+ #
9
+ # TokenManager provides production-grade token automation by:
10
+ # - Generating new tokens via TOTP when needed
11
+ # - Renewing tokens before expiry (5-minute buffer)
12
+ # - Falling back to full generation if renewal fails
13
+ # - Thread-safe token operations via Monitor lock
14
+ #
15
+ # @example Enable auto token management
16
+ # client = DhanHQ::Client.new(api_type: :order_api)
17
+ # client.enable_auto_token_management!(
18
+ # dhan_client_id: ENV["DHAN_CLIENT_ID"],
19
+ # pin: ENV["DHAN_PIN"],
20
+ # totp_secret: ENV["DHAN_TOTP_SECRET"]
21
+ # )
22
+ #
23
+ # @example Manual usage
24
+ # manager = TokenManager.new(
25
+ # dhan_client_id: "123",
26
+ # pin: "1234",
27
+ # totp_secret: "SECRET"
28
+ # )
29
+ # manager.ensure_valid_token! # Auto-generates or renews as needed
30
+ #
31
+ # @see Auth::TokenGenerator for TOTP-based token generation
32
+ # @see Auth::TokenRenewal for token renewal logic
33
+ class TokenManager
34
+ def initialize(dhan_client_id:, pin:, totp_secret:)
35
+ @dhan_client_id = dhan_client_id
36
+ @pin = pin
37
+ @totp_secret = totp_secret
38
+
39
+ @token = nil
40
+ @lock = Monitor.new
41
+ end
42
+
43
+ def generate!
44
+ @lock.synchronize do
45
+ token = Auth::TokenGenerator.new.generate(
46
+ dhan_client_id: @dhan_client_id,
47
+ pin: @pin,
48
+ totp_secret: @totp_secret
49
+ )
50
+
51
+ apply_token(token)
52
+ end
53
+ end
54
+
55
+ def ensure_valid_token!
56
+ return generate! unless @token
57
+
58
+ return unless @token.needs_refresh?
59
+
60
+ refresh!
61
+ end
62
+
63
+ def refresh!
64
+ @lock.synchronize do
65
+ return generate! unless @token
66
+
67
+ renewal = Auth::TokenRenewal.new.renew
68
+ apply_token(renewal)
69
+ rescue Errors::AuthenticationError
70
+ generate!
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def apply_token(token)
77
+ @token = token
78
+
79
+ DhanHQ.configure do |config|
80
+ config.access_token = token.access_token
81
+ config.client_id = token.client_id if token.client_id.to_s.strip != ""
82
+ end
83
+
84
+ token
85
+ end
86
+ end
87
+ end
88
+ end