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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +50 -1
- data/CODE_REVIEW_ISSUES.md +2 -2
- data/GUIDE.md +2 -2
- data/README.md +194 -741
- data/REVIEW_SUMMARY.md +2 -2
- data/{README1.md → docs/ARCHIVE_README.md} +4 -4
- data/docs/AUTHENTICATION.md +116 -2
- data/docs/CONFIGURATION.md +109 -0
- data/docs/SUPER_ORDERS.md +284 -0
- data/docs/TESTING_GUIDE.md +8 -8
- data/docs/TROUBLESHOOTING.md +117 -0
- data/docs/WEBSOCKET_PROTOCOL.md +154 -0
- data/docs/live_order_updates.md +2 -2
- data/docs/rails_integration.md +7 -7
- data/docs/standalone_ruby_websocket_integration.md +24 -24
- data/docs/technical_analysis.md +1 -1
- data/docs/websocket_integration.md +4 -4
- data/examples/comprehensive_websocket_examples.rb +2 -2
- data/examples/instrument_finder_test.rb +2 -2
- data/examples/market_depth_example.rb +2 -2
- data/examples/market_feed_example.rb +2 -2
- data/examples/order_update_example.rb +2 -2
- data/examples/trading_fields_example.rb +2 -2
- data/lib/DhanHQ/auth/token_generator.rb +33 -0
- data/lib/DhanHQ/auth/token_manager.rb +88 -0
- data/lib/DhanHQ/auth/token_renewal.rb +25 -0
- data/lib/DhanHQ/auth.rb +91 -31
- data/lib/DhanHQ/client.rb +42 -2
- data/lib/DhanHQ/configuration.rb +2 -2
- data/lib/DhanHQ/contracts/order_contract.rb +0 -23
- data/lib/DhanHQ/contracts/trade_by_order_id_contract.rb +12 -0
- data/lib/DhanHQ/contracts/trade_contract.rb +0 -65
- data/lib/DhanHQ/contracts/trade_history_contract.rb +52 -0
- data/lib/DhanHQ/core/auth_api.rb +21 -0
- data/lib/DhanHQ/helpers/request_helper.rb +1 -1
- data/lib/DhanHQ/models/alert_order.rb +22 -0
- data/lib/DhanHQ/models/edis.rb +110 -0
- data/lib/DhanHQ/models/kill_switch.rb +22 -0
- data/lib/DhanHQ/models/margin.rb +49 -0
- data/lib/DhanHQ/models/pnl_exit.rb +130 -0
- data/lib/DhanHQ/models/position.rb +22 -0
- data/lib/DhanHQ/models/postback.rb +123 -0
- data/lib/DhanHQ/models/token_response.rb +88 -0
- data/lib/DhanHQ/resources/kill_switch.rb +8 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
- data/lib/DhanHQ/resources/positions.rb +8 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/dhan_hq.rb +31 -81
- metadata +46 -4
- 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
|
+
```
|
data/docs/live_order_updates.md
CHANGED
|
@@ -194,8 +194,8 @@ client.start
|
|
|
194
194
|
### Environment Variables
|
|
195
195
|
```bash
|
|
196
196
|
# Required
|
|
197
|
-
|
|
198
|
-
|
|
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
|
data/docs/rails_integration.md
CHANGED
|
@@ -29,8 +29,8 @@ boot successfully:
|
|
|
29
29
|
|
|
30
30
|
| Variable | Description |
|
|
31
31
|
| --- | --- |
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
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['
|
|
65
|
-
ENV['
|
|
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['
|
|
81
|
-
ENV['
|
|
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
|
|
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["
|
|
35
|
-
config.access_token = ENV["
|
|
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
|
|
63
|
-
export
|
|
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
|
|
78
|
-
export
|
|
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["
|
|
136
|
-
config.access_token = ENV["
|
|
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["
|
|
268
|
-
config.access_token = ENV["
|
|
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["
|
|
398
|
-
config.access_token = ENV["
|
|
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=
|
|
546
|
-
Environment=
|
|
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["
|
|
566
|
-
config.access_token = ENV["
|
|
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["
|
|
783
|
-
config.access_token = ENV["
|
|
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["
|
|
1065
|
-
config.access_token = ENV["
|
|
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["
|
|
1268
|
-
config.access_token = ENV["
|
|
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["
|
|
1387
|
-
config.access_token = ENV["
|
|
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
|
|
data/docs/technical_analysis.md
CHANGED
|
@@ -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: `
|
|
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["
|
|
32
|
-
config.access_token = ENV["
|
|
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["
|
|
791
|
-
config.access_token = ENV["
|
|
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["
|
|
16
|
-
config.access_token = ENV["
|
|
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["
|
|
12
|
-
config.access_token = ENV["
|
|
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["
|
|
14
|
-
config.access_token = ENV["
|
|
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["
|
|
14
|
-
config.access_token = ENV["
|
|
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["
|
|
14
|
-
config.access_token = ENV["
|
|
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["
|
|
13
|
-
config.access_token = ENV["
|
|
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
|