DhanHQ 3.0.1 → 3.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/ARCHITECTURE.md +47 -1
- data/CHANGELOG.md +98 -0
- data/README.md +392 -6
- data/docs/AUTHENTICATION.md +3 -5
- data/docs/CONFIGURATION.md +3 -2
- data/docs/CONSTANTS_REFERENCE.md +8 -6
- data/docs/ENDPOINTS_AND_SANDBOX.md +1 -0
- data/docs/LIVE_ORDER_UPDATES.md +5 -10
- data/docs/RAILS_INTEGRATION.md +1 -1
- data/docs/RELEASE_GUIDE.md +13 -2
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +2 -2
- data/docs/WEBSOCKET_INTEGRATION.md +13 -20
- data/docs/WEBSOCKET_PROTOCOL.md +7 -3
- data/exe/dhanhq-mcp +7 -0
- data/lib/DhanHQ/agent/key_coercion.rb +36 -0
- data/lib/DhanHQ/agent/tool.rb +37 -0
- data/lib/DhanHQ/agent/tool_catalogue.rb +180 -0
- data/lib/DhanHQ/agent/tool_handlers.rb +116 -0
- data/lib/DhanHQ/agent/tool_registry.rb +17 -212
- data/lib/DhanHQ/agent/tool_schemas.rb +160 -0
- data/lib/DhanHQ/client.rb +58 -2
- data/lib/DhanHQ/concerns/order_audit.rb +74 -1
- data/lib/DhanHQ/configuration.rb +70 -1
- data/lib/DhanHQ/constants.rb +104 -2
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +1 -9
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/global_stocks_estimator_contract.rb +28 -0
- data/lib/DhanHQ/contracts/global_stocks_modify_order_contract.rb +26 -0
- data/lib/DhanHQ/contracts/global_stocks_order_contract.rb +33 -0
- data/lib/DhanHQ/contracts/global_stocks_place_order_contract.rb +75 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +1 -21
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/multi_order_contract.rb +74 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/trade_history_contract.rb +1 -8
- data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
- data/lib/DhanHQ/dry_run/ledger.rb +71 -0
- data/lib/DhanHQ/dry_run/simulator.rb +140 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +23 -0
- data/lib/DhanHQ/mcp/server.rb +172 -9
- data/lib/DhanHQ/models/alert_order.rb +5 -2
- data/lib/DhanHQ/models/global_stocks/funds.rb +56 -0
- data/lib/DhanHQ/models/global_stocks/holding.rb +101 -0
- data/lib/DhanHQ/models/global_stocks/margin.rb +54 -0
- data/lib/DhanHQ/models/global_stocks/market_status.rb +63 -0
- data/lib/DhanHQ/models/global_stocks/order.rb +189 -0
- data/lib/DhanHQ/models/global_stocks/order_estimate.rb +61 -0
- data/lib/DhanHQ/models/global_stocks/trade.rb +74 -0
- data/lib/DhanHQ/models/instrument.rb +44 -14
- data/lib/DhanHQ/models/margin.rb +5 -1
- data/lib/DhanHQ/models/multi_order.rb +130 -0
- data/lib/DhanHQ/rate_limiter.rb +5 -3
- data/lib/DhanHQ/resources/alert_orders.rb +1 -0
- data/lib/DhanHQ/resources/forever_orders.rb +1 -0
- data/lib/DhanHQ/resources/global_stocks/funds.rb +25 -0
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +70 -0
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/orders.rb +112 -0
- data/lib/DhanHQ/resources/global_stocks/trades.rb +31 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +1 -0
- data/lib/DhanHQ/resources/multi_orders.rb +58 -0
- data/lib/DhanHQ/resources/orders.rb +2 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
- data/lib/DhanHQ/resources/super_orders.rb +1 -0
- data/lib/DhanHQ/resources/twap_orders.rb +1 -0
- data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
- data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
- data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
- data/lib/DhanHQ/risk/pipeline.rb +8 -1
- data/lib/DhanHQ/skills/base.rb +54 -3
- data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +10 -13
- data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +15 -19
- data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
- data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +5 -8
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +8 -6
- data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +13 -13
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/write_paths.rb +57 -0
- data/lib/DhanHQ/ws/client.rb +117 -2
- data/lib/DhanHQ/ws/connection.rb +40 -11
- data/lib/DhanHQ/ws/sub_state.rb +14 -0
- data/lib/dhan_hq.rb +47 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
- metadata +41 -15
- data/.rspec +0 -3
- data/.rubocop.yml +0 -48
- data/.rubocop_todo.yml +0 -217
- data/AGENTS.md +0 -23
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -14
- data/TAGS +0 -10
- data/core +0 -0
- data/diagram.html +0 -184
- data/skills/dhanhq-ruby/SKILL.md +0 -74
- data/skills/dhanhq-ruby/references/market_data.md +0 -3
- data/skills/dhanhq-ruby/references/orders.md +0 -7
- data/watchlist.csv +0 -3
data/README.md
CHANGED
|
@@ -117,7 +117,11 @@ For the full dependency flow and extension pattern, see [ARCHITECTURE.md](ARCHIT
|
|
|
117
117
|
- **Instrument convenience methods** — `.ltp`, `.ohlc`, `.option_chain` directly on instruments
|
|
118
118
|
- **Order audit logging** — every order attempt logs machine, IP, environment, and correlation ID as structured JSON
|
|
119
119
|
- **Live trading guard** — prevents accidental order placement unless `ENV["LIVE_TRADING"]="true"`
|
|
120
|
-
- **
|
|
120
|
+
- **Dry-run mode** — `config.dry_run = true` validates and logs every write while reads stay live, so a full strategy can be rehearsed against real prices
|
|
121
|
+
- **No duplicate orders on retry** — transient failures on order writes are surfaced, not blindly retried
|
|
122
|
+
- **Global Stocks (US equities)** — separate order book, holdings, trades, USD funds, market status, charge estimator and margin calculator
|
|
123
|
+
- **Basket orders** — up to 15 orders in a single request, with per-leg acceptance results
|
|
124
|
+
- **Full REST coverage** — Orders, Trades, Forever Orders, Super Orders, Multi (basket) Orders, Positions, Holdings, Funds, HistoricalData, OptionChain, MarketFeed, EDIS, Kill Switch, P&L Exit, Alert Orders, Margin Calculator, IP Setup, Global Stocks
|
|
121
125
|
- **P&L Based Exit** — automatic position exit on profit/loss thresholds
|
|
122
126
|
- **Postback parser** — parse Dhan webhook payloads with `Postback.parse` and status predicates
|
|
123
127
|
- **EDIS model** — ORM-style T-PIN, form, and status inquiry for delivery instruction slips
|
|
@@ -127,10 +131,12 @@ For the full dependency flow and extension pattern, see [ARCHITECTURE.md](ARCHIT
|
|
|
127
131
|
## Reliability & Safety
|
|
128
132
|
|
|
129
133
|
- retry-on-401 with token refresh
|
|
130
|
-
- WebSocket auto-reconnect and
|
|
134
|
+
- WebSocket auto-reconnect, backoff, and automatic re-subscription
|
|
131
135
|
- 429 rate-limit protection
|
|
132
136
|
- live trading guard via `LIVE_TRADING=true`
|
|
133
137
|
- structured order audit logs
|
|
138
|
+
- dry-run mode via `DHAN_DRY_RUN=true`
|
|
139
|
+
- order writes are never silently retried, so a timeout cannot become two orders
|
|
134
140
|
|
|
135
141
|
See [ARCHITECTURE.md](ARCHITECTURE.md), [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md), and [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for the deeper implementation details.
|
|
136
142
|
|
|
@@ -219,6 +225,95 @@ LIVE_TRADING=false # or simply omit
|
|
|
219
225
|
|
|
220
226
|
Attempting to place an order without `LIVE_TRADING=true` raises `DhanHQ::LiveTradingDisabledError`.
|
|
221
227
|
|
|
228
|
+
### Dry-Run Mode
|
|
229
|
+
|
|
230
|
+
`dry_run` suppresses every request that would change account state — order placement,
|
|
231
|
+
modification, cancellation, position exits, kill switch, P&L exit — while letting reads
|
|
232
|
+
through untouched. Market data, the option chain, positions and holdings stay live, so a
|
|
233
|
+
strategy can be rehearsed end-to-end against real prices without any risk of an order
|
|
234
|
+
reaching the exchange.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
DhanHQ.configure do |config|
|
|
238
|
+
config.dry_run = true # or DHAN_DRY_RUN=true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
order = DhanHQ::Models::Order.place(
|
|
242
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
243
|
+
exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
|
|
244
|
+
product_type: DhanHQ::Constants::ProductType::INTRADAY,
|
|
245
|
+
order_type: DhanHQ::Constants::OrderType::LIMIT,
|
|
246
|
+
validity: DhanHQ::Constants::Validity::DAY,
|
|
247
|
+
security_id: "11536", quantity: 5, price: 1500.0
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Suppressed writes are logged at WARN level with the full payload, and order placements come
|
|
252
|
+
back with a simulated `orderId` so downstream code paths run to completion:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{ "event": "DHAN_DRY_RUN", "method": "POST", "path": "/v2/orders", "payload": { "…": "…" } }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Note that a POST is not automatically a write on this API: the option chain, market feed,
|
|
259
|
+
historical charts and the margin calculators are read-only POSTs, and dry-run leaves them
|
|
260
|
+
alone.
|
|
261
|
+
|
|
262
|
+
This works through the high-level models too, not just `Client#request`. Because
|
|
263
|
+
`Order.place` re-fetches after placing, the SDK records each simulated order and replays reads
|
|
264
|
+
for its `DRYRUN-…` id locally — so a rehearsal issues no request for a fake order, and the
|
|
265
|
+
model you get back carries the fields you submitted:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
order.order_id # => "DRYRUN-9F7BEB55D0F9"
|
|
269
|
+
order.security_id # => "11536"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`dry_run` complements `Agent::OrderPreview` — preview validates a single order and returns a
|
|
273
|
+
summary, while `dry_run` covers every write path in the SDK, including skills and the MCP
|
|
274
|
+
tools.
|
|
275
|
+
|
|
276
|
+
### Order Retries and Duplicate Protection
|
|
277
|
+
|
|
278
|
+
The DhanHQ API has no idempotency key. A `POST /v2/orders` that times out may well have
|
|
279
|
+
reached the exchange, so retrying it can place a second, real order. By default this SDK
|
|
280
|
+
therefore **does not** retry non-idempotent writes — the transient error is raised to you:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
begin
|
|
284
|
+
DhanHQ::Models::Order.place(...)
|
|
285
|
+
rescue DhanHQ::NetworkError
|
|
286
|
+
# The order may or may not have been accepted. Reconcile before resubmitting.
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Reads, and read-only POSTs like the option chain, are still retried with exponential backoff.
|
|
291
|
+
|
|
292
|
+
To reconcile after a timeout, place orders with a correlation id and look the order up by it:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
DhanHQ.configure do |config|
|
|
296
|
+
config.auto_correlation_id = true # or DHAN_AUTO_CORRELATION_ID=true
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
With this on, any order placement missing a `correlationId` gets one (`dhq-<hex>`), which you
|
|
301
|
+
can then resolve via `GET /v2/orders/external/{correlation-id}`:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
DhanHQ::Resources::Orders.new.by_correlation("dhq-2f9c1a…")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
It is off by default because it changes the request body, which would break callers that match
|
|
308
|
+
recorded fixtures on the exact payload. An explicit `correlation_id` is always preserved.
|
|
309
|
+
|
|
310
|
+
If you would rather have the old auto-retry behaviour, opt back in — accepting the duplicate
|
|
311
|
+
risk:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
DhanHQ.configure { |config| config.retry_non_idempotent_writes = true }
|
|
315
|
+
```
|
|
316
|
+
|
|
222
317
|
### Order Audit Logging
|
|
223
318
|
|
|
224
319
|
Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:
|
|
@@ -342,6 +437,45 @@ DhanHQ::WS::MarketDepth.connect(symbols: [
|
|
|
342
437
|
end
|
|
343
438
|
```
|
|
344
439
|
|
|
440
|
+
### Lifecycle Hooks and Health Checks
|
|
441
|
+
|
|
442
|
+
Subscriptions are restored automatically on reconnect — the server keeps no subscription state
|
|
443
|
+
across connections, so the client replays the desired set on every new session. Hook `:reconnect`
|
|
444
|
+
when you need to re-seed derived state (candle builders, caches) after a gap:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
client = DhanHQ::WS.connect(mode: :ticker) { |tick| handle(tick) }
|
|
448
|
+
|
|
449
|
+
client.on(:open) { |info| logger.info "feed open, #{info[:resubscribed].size} restored" }
|
|
450
|
+
client.on(:reconnect) { |info| logger.warn "reconnect ##{info[:attempt]}"; candles.reset! }
|
|
451
|
+
client.on(:close) { |info| logger.warn "closed #{info[:code]} #{info[:reason]}" }
|
|
452
|
+
client.on(:error) { |message| logger.error message }
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
A feed can be connected and still be dead — the socket stays open while the server stops
|
|
456
|
+
publishing. For long-running daemons, watch frame arrival rather than the socket alone:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
client.connected? # socket state
|
|
460
|
+
client.healthy? # connected AND delivering frames (default: within 45s)
|
|
461
|
+
client.healthy?(stale_after: 20) # tighter threshold
|
|
462
|
+
client.last_message_at # Time of the most recent frame
|
|
463
|
+
client.seconds_since_last_message
|
|
464
|
+
client.reconnect_count
|
|
465
|
+
client.subscriptions # ["NSE_EQ:11536", …] — survives reconnects
|
|
466
|
+
|
|
467
|
+
client.health
|
|
468
|
+
# => { mode: :ticker, started: true, connected: true, healthy: true,
|
|
469
|
+
# connected_at: …, last_message_at: …, seconds_since_last_message: 0.4,
|
|
470
|
+
# reconnect_count: 2, subscription_count: 37 }
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`health` is shaped for a monitoring endpoint or a periodic log line.
|
|
474
|
+
|
|
475
|
+
> **Connection limit:** Dhan allows **5 concurrent WebSocket connections per client id**, with
|
|
476
|
+
> 5,000 instruments per connection and 100 instruments per subscribe frame. Running several
|
|
477
|
+
> strategies in separate processes counts against the same limit — a 6th connection is refused.
|
|
478
|
+
|
|
345
479
|
### Cleanup
|
|
346
480
|
|
|
347
481
|
```ruby
|
|
@@ -373,6 +507,110 @@ DhanHQ::Models::SuperOrder.create(
|
|
|
373
507
|
|
|
374
508
|
---
|
|
375
509
|
|
|
510
|
+
## Basket Orders (Multi Order)
|
|
511
|
+
|
|
512
|
+
Place up to 15 unconditional orders in a single request. Each leg carries a `sequence` that
|
|
513
|
+
identifies it in the response, so partial acceptance is visible:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
result = DhanHQ::Models::MultiOrder.place([
|
|
517
|
+
{ sequence: "1", transaction_type: "BUY", exchange_segment: "NSE_EQ", product_type: "CNC",
|
|
518
|
+
order_type: "LIMIT", validity: "DAY", security_id: "11536", quantity: 1, price: 1500.0 },
|
|
519
|
+
{ sequence: "2", transaction_type: "BUY", exchange_segment: "NSE_EQ", product_type: "CNC",
|
|
520
|
+
order_type: "MARKET", validity: "DAY", security_id: "1333", quantity: 2 }
|
|
521
|
+
])
|
|
522
|
+
|
|
523
|
+
result.all_accepted? # => true
|
|
524
|
+
result.order_ids # => ["112111182198", "112111182199"]
|
|
525
|
+
result.rejected # legs the exchange refused
|
|
526
|
+
result.partially_accepted? # some in, some out
|
|
527
|
+
result.for_sequence("1").order_id # look a leg up by its sequence
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Every leg is validated before anything is sent, and each one gets its own audit log line.
|
|
531
|
+
Requires `LIVE_TRADING=true` like any other order path.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Global Stocks (US Equities)
|
|
536
|
+
|
|
537
|
+
US stocks are a **separate book** from domestic NSE/BSE trading: their own order book,
|
|
538
|
+
holdings, trades, and a USD fund limit. They are namespaced under `GlobalStocks` so a USD
|
|
539
|
+
position can never be mistaken for an INR one.
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
# Is the US market open?
|
|
543
|
+
DhanHQ::Models::GlobalStocks::MarketStatus.fetch.open?
|
|
544
|
+
|
|
545
|
+
# USD balance and portfolio
|
|
546
|
+
DhanHQ::Models::GlobalStocks::Funds.fetch.available_cash
|
|
547
|
+
DhanHQ::Models::GlobalStocks::Holding.all.each do |h|
|
|
548
|
+
puts "#{h.trading_symbol}: #{h.quantity} @ $#{h.avg_cost_price} (#{h.gain_percentage}%)"
|
|
549
|
+
end
|
|
550
|
+
DhanHQ::Models::GlobalStocks::Holding.total_current_value
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Placing US orders
|
|
554
|
+
|
|
555
|
+
Global Stocks orders carry **no exchange segment, product type, or validity** — and quantity
|
|
556
|
+
is a float, because fractional shares are supported:
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
DhanHQ::Models::GlobalStocks::Order.place(
|
|
560
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
561
|
+
order_type: DhanHQ::Constants::GlobalStocks::OrderType::LIMIT,
|
|
562
|
+
security_id: "AAPL",
|
|
563
|
+
quantity: 0.5, # fractional shares
|
|
564
|
+
price: 180.0
|
|
565
|
+
)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`AMOUNT` is a notional order type unique to this book — you name a dollar value instead of a
|
|
569
|
+
share count:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
DhanHQ::Models::GlobalStocks::Order.place(
|
|
573
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
574
|
+
order_type: DhanHQ::Constants::GlobalStocks::OrderType::AMOUNT,
|
|
575
|
+
security_id: "AAPL",
|
|
576
|
+
amount: 500.0 # buy $500 worth
|
|
577
|
+
)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Pre-trade checks
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
params = { security_id: "AAPL", transaction_type: "BUY", price: 180.0, quantity: 10 }
|
|
584
|
+
|
|
585
|
+
estimate = DhanHQ::Models::GlobalStocks::OrderEstimate.calculate(params)
|
|
586
|
+
estimate.total_charges # brokerage + exchange + GST + turnover + other
|
|
587
|
+
|
|
588
|
+
margin = DhanHQ::Models::GlobalStocks::Margin.calculate(params)
|
|
589
|
+
margin.sufficient? # false when the account is short
|
|
590
|
+
margin.total_margin
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Order and trade books
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
DhanHQ::Models::GlobalStocks::Order.all # today's US order book
|
|
597
|
+
DhanHQ::Models::GlobalStocks::Order.find(order_id)
|
|
598
|
+
DhanHQ::Models::GlobalStocks::Trade.all # today's US fills
|
|
599
|
+
DhanHQ::Models::GlobalStocks::Trade.find_by_security_id("AAPL")
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Writes go through the same `LIVE_TRADING` gate, audit logging, and dry-run handling as
|
|
603
|
+
domestic orders. The pre-trade `Risk::Pipeline` is deliberately **not** applied: its checks
|
|
604
|
+
resolve instruments from the Indian scrip master and encode NSE/BSE rules (lot sizes, ASM/GSM
|
|
605
|
+
surveillance, F&O product support, IST market hours), none of which apply to US equities. Use
|
|
606
|
+
`Margin#sufficient?` and `OrderEstimate` as the pre-trade gate here instead.
|
|
607
|
+
|
|
608
|
+
The Global Stocks live feed (`wss://global-stocks-api-feed.dhan.co`, segment `INX_EQ`, code
|
|
609
|
+
`14`) is documented in `DhanHQ::Constants::GlobalStocks`; its binary packet decoder is not yet
|
|
610
|
+
implemented.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
376
614
|
## Real-World Example: NIFTY Trend Monitor
|
|
377
615
|
|
|
378
616
|
```ruby
|
|
@@ -421,13 +659,18 @@ Need initializers, service objects, ActionCable wiring, and background workers?
|
|
|
421
659
|
|
|
422
660
|
These scripts are designed around user goals rather than API surfaces:
|
|
423
661
|
|
|
424
|
-
| Example |
|
|
425
|
-
| ------- |
|
|
426
|
-
| [examples/basic_trading_bot.rb](examples/basic_trading_bot.rb) |
|
|
427
|
-
| [examples/
|
|
662
|
+
| Example | What it covers |
|
|
663
|
+
| ------- | --------------- |
|
|
664
|
+
| [examples/basic_trading_bot.rb](examples/basic_trading_bot.rb) | Trading bot scaffold with live-trading guard |
|
|
665
|
+
| [examples/comprehensive_websocket_examples.rb](examples/comprehensive_websocket_examples.rb) | WebSocket mode coverage and timeout handling |
|
|
428
666
|
| [examples/options_watchlist.rb](examples/options_watchlist.rb) | Build a live options watchlist with index quotes and option-chain context |
|
|
429
667
|
| [examples/market_feed_example.rb](examples/market_feed_example.rb) | Subscribe to major market indices over WebSocket |
|
|
668
|
+
| [examples/market_depth_example.rb](examples/market_depth_example.rb) | Market depth streaming example |
|
|
430
669
|
| [examples/live_order_updates.rb](examples/live_order_updates.rb) | Track order lifecycle events in real time |
|
|
670
|
+
| [examples/order_update_example.rb](examples/order_update_example.rb) | Single-session order-update flow |
|
|
671
|
+
| [examples/portfolio_monitor.rb](examples/portfolio_monitor.rb) | Snapshot funds, holdings, and positions for a monitoring script |
|
|
672
|
+
| [examples/trading_fields_example.rb](examples/trading_fields_example.rb) | Dhan order-field mappings and constants example |
|
|
673
|
+
| [examples/instrument_finder_test.rb](examples/instrument_finder_test.rb) | Instrument search/resolution troubleshooting |
|
|
431
674
|
|
|
432
675
|
For search-driven discovery and onboarding content, see:
|
|
433
676
|
|
|
@@ -471,6 +714,149 @@ For search-driven discovery and onboarding content, see:
|
|
|
471
714
|
|
|
472
715
|
---
|
|
473
716
|
|
|
717
|
+
## MCP Server (AI Agent Integration)
|
|
718
|
+
|
|
719
|
+
DhanHQ includes a built-in [Model Context Protocol](https://modelcontextprotocol.io) server that lets AI coding agents (Claude Code, Codex, OpenCode, Cursor) interact with your Dhan account directly from the editor or CLI.
|
|
720
|
+
|
|
721
|
+
### Quick Start
|
|
722
|
+
|
|
723
|
+
```bash
|
|
724
|
+
# 1. Configure credentials
|
|
725
|
+
export DHAN_CLIENT_ID="your_client_id"
|
|
726
|
+
export DHAN_ACCESS_TOKEN="your_access_token"
|
|
727
|
+
|
|
728
|
+
# 2. Start the server (stdio)
|
|
729
|
+
bundle exec dhanhq-mcp
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Claude Desktop config (`claude_desktop_config.json`):
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{
|
|
736
|
+
"mcpServers": {
|
|
737
|
+
"dhanhq": {
|
|
738
|
+
"command": "bundle",
|
|
739
|
+
"args": ["exec", "dhanhq-mcp"],
|
|
740
|
+
"env": {
|
|
741
|
+
"DHAN_CLIENT_ID": "your_client_id",
|
|
742
|
+
"DHAN_ACCESS_TOKEN": "your_access_token"
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### MCP Features
|
|
750
|
+
|
|
751
|
+
| Feature | Description |
|
|
752
|
+
| ------- | ----------- |
|
|
753
|
+
| **Tools** | 32 total: 12 domestic primitives (profile, funds, holdings, positions, order history, order preview/place/cancel, instruments, market feed) + 9 Global Stocks and basket tools (`dhan_global_*`, `dhan_multi_order`) + 11 skill-derived tools (`dhan_skill_*` — one per builtin strategy in [Skills System](#skills-system) below) |
|
|
754
|
+
| **Resources** | 6 URI-addressable data endpoints: `dhanhq://account/profile`, `dhanhq://account/funds`, `dhanhq://account/holdings`, `dhanhq://account/positions`, `dhanhq://account/orders`, `dhanhq://market/capabilities` |
|
|
755
|
+
| **Prompts** | 5 pre-built AI prompts: `portfolio_summary`, `market_analysis`, `risk_report`, `order_preview`, `suggest_strategy` |
|
|
756
|
+
|
|
757
|
+
### Security & Policy
|
|
758
|
+
|
|
759
|
+
```ruby
|
|
760
|
+
# Read-only mode (no order placement)
|
|
761
|
+
DhanHQ::Agent::Policy.read_only
|
|
762
|
+
|
|
763
|
+
# Scope-based policy from env (DHANHQ_AGENT_SCOPES)
|
|
764
|
+
DhanHQ::Agent::Policy.from_env
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
The policy engine respects `DHANHQ_MCP_ENABLE_WRITES` and `LIVE_TRADING` env vars. Write operations are blocked by default — explicit opt-in required.
|
|
768
|
+
|
|
769
|
+
## Skills System
|
|
770
|
+
|
|
771
|
+
Skills are reusable, composable trading strategies. DhanHQ ships with **11 builtin skills** and a registry for discovery and invocation.
|
|
772
|
+
|
|
773
|
+
### Builtin Skills
|
|
774
|
+
|
|
775
|
+
| Skill | Type | Description |
|
|
776
|
+
| ----- | ---- | ----------- |
|
|
777
|
+
| `buy_atm_call` | Single-leg | Buy ATM call option |
|
|
778
|
+
| `square_off_all` | Action | Square off all open positions |
|
|
779
|
+
| `square_off_position` | Action | Square off a specific position |
|
|
780
|
+
| `iron_condor` | Multi-leg | Sell OTM put + buy further OTM put + sell OTM call + buy further OTM call |
|
|
781
|
+
| `strangle` | Multi-leg | Buy OTM put + buy OTM call |
|
|
782
|
+
| `covered_call` | Multi-leg | Buy equity + sell OTM call |
|
|
783
|
+
| `bull_put_spread` | Multi-leg | Sell OTM put + buy further OTM put |
|
|
784
|
+
| `bear_call_spread` | Multi-leg | Sell OTM call + buy further OTM call |
|
|
785
|
+
| `protective_put` | Multi-leg | Buy equity + buy OTM put |
|
|
786
|
+
| `straddle` | Multi-leg | Buy ATM call + buy ATM put |
|
|
787
|
+
| `market_data_summarizer` | Read-only | Summarize technicals and/or option chain for a symbol |
|
|
788
|
+
|
|
789
|
+
### Using Skills
|
|
790
|
+
|
|
791
|
+
```ruby
|
|
792
|
+
# Register all builtin skills
|
|
793
|
+
DhanHQ::Skills::Registry.load_builtins
|
|
794
|
+
|
|
795
|
+
# Find a skill by name
|
|
796
|
+
skill = DhanHQ::Skills::Registry.find("covered_call")
|
|
797
|
+
|
|
798
|
+
# Invoke — returns an intent hash (trade_type, legs, risk metadata)
|
|
799
|
+
result = skill.call(symbol: "RELIANCE", expiry: "2026-06-25")
|
|
800
|
+
# => { intent: { trade_type: "COVERED_CALL", legs: [...], total_premium: ..., break_even: ..., note: "..." } }
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Skills return intent hashes (not executed trades), keeping a human-in-the-loop safety pattern.
|
|
804
|
+
|
|
805
|
+
## AI Integration
|
|
806
|
+
|
|
807
|
+
DhanHQ provides prompt helpers and risk reporting for LLM-powered trading agents.
|
|
808
|
+
|
|
809
|
+
### Prompt Helpers
|
|
810
|
+
|
|
811
|
+
```ruby
|
|
812
|
+
require 'dhan_hq/ai'
|
|
813
|
+
|
|
814
|
+
# Portfolio summary for AI consumption
|
|
815
|
+
DhanHQ::AI::PromptHelpers.portfolio_summary
|
|
816
|
+
# => "Portfolio Summary\n━━━━━━━━━━━━━\nFunds: ₹1,00,000.00\n..."
|
|
817
|
+
|
|
818
|
+
# Risk report
|
|
819
|
+
DhanHQ::AI::PromptHelpers.risk_report
|
|
820
|
+
# => "🔴 RISK ALERT: 394.6% drawdown..."
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Risk Pipeline
|
|
824
|
+
|
|
825
|
+
```ruby
|
|
826
|
+
# Run pre-trade risk checks
|
|
827
|
+
DhanHQ::Risk::Pipeline.run!(
|
|
828
|
+
instrument: instrument,
|
|
829
|
+
args: { quantity: 25, price: 24_500 },
|
|
830
|
+
type: :fno,
|
|
831
|
+
now: Time.now
|
|
832
|
+
)
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Available checks:
|
|
836
|
+
- **TradingPermission** — blocks instruments where trading is disabled (`buy_sell_indicator != "A"`)
|
|
837
|
+
- **AsmGsm** — blocks ASM/GSM restricted instruments
|
|
838
|
+
- **ProductSupport** — validates bracket/cover order support for the instrument
|
|
839
|
+
- **OrderType** — restricts to `MARKET`/`LIMIT` order types
|
|
840
|
+
- **Quantity** — max 10 units / ₹1,00,000 notional (an agent-safety limit, not a general trading cap)
|
|
841
|
+
- **MarketHours** — verifies market is open (9:15 AM–3:30 PM IST)
|
|
842
|
+
- **PositionLimits** — max 20 concurrent open positions
|
|
843
|
+
- **Concentration** — max 25% of available balance in a single symbol
|
|
844
|
+
- **Options** (options only) — index-only, requires stop loss + target + risk-reward
|
|
845
|
+
- **MaxLoss** (daily) — daily loss limit (default ₹50,000)
|
|
846
|
+
|
|
847
|
+
Checks raise `DhanHQ::RiskViolation` with human-readable messages, safe for AI parsing. Wired into every order-placing path via `DhanHQ::Concerns::OrderAudit#run_risk_checks!` (Orders, SuperOrders, ForeverOrders, AlertOrders, TwapOrders, IcebergOrders, PnL Exit) and into the `dhan_place_order` MCP tool.
|
|
848
|
+
|
|
849
|
+
### Known Limitations
|
|
850
|
+
|
|
851
|
+
This gem's core REST/WS client (orders, positions, funds, market data) is mature and already depended on in production by other repos in this workspace. The MCP server, Skills system, and Risk pipeline are newer and have now been verified live end to end — including the full write path — against real Dhan accounts, a real independent MCP client, and a real order-matching engine — see [CHANGELOG.md](CHANGELOG.md#known-limitations) for details:
|
|
852
|
+
|
|
853
|
+
- Read path (profile/funds/holdings/positions/orders/instrument lookup/option chains, all 11 skills' intent-building, WebSocket streaming) — live-verified.
|
|
854
|
+
- Write path — `dhan_place_order`, `dhan_cancel_order`, `dhan_skill_square_off_all`, and `dhan_skill_square_off_position` all verified end-to-end through the MCP-gated path (instrument resolution, full risk pipeline, audit logging, real execution). Dhan's own sandbox never executes real fills (by design, per Dhan's docs), so fills were exercised against `simulators/paper_exchange`'s real matching engine via a throwaway Dhan-API-compatible adapter. Found and fixed a real bug in the process: both square-off skills used `p[:net_quantity]`-style hash indexing on `Position` (which has no `[]` method), silently finding zero positions to close every time — invisible until a real position finally existed to test against.
|
|
855
|
+
- Risk checks that read portfolio state (`Concentration`, `PositionLimits`, `MaxLoss`) have only been observed against zero/low-balance accounts and a single simulated position — not a large multi-symbol portfolio.
|
|
856
|
+
- The sandbox environment's security-ID catalog is disconnected from the production instrument master — resolve IDs from sandbox order history, not `Instrument.find`, when testing against sandbox.
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
474
860
|
## Best Practices
|
|
475
861
|
|
|
476
862
|
- Keep `on(:tick)` handlers **non-blocking** — push heavy work to a queue/thread
|
data/docs/AUTHENTICATION.md
CHANGED
|
@@ -54,7 +54,7 @@ If the token was **generated from Dhan Web** (not API key flow), you can refresh
|
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
56
|
# Returns a hash with API response (e.g. accessToken, expiryTime). Use the new token for subsequent requests.
|
|
57
|
-
response = DhanHQ::Auth.renew_token(current_access_token, client_id)
|
|
57
|
+
response = DhanHQ::Auth.renew_token(access_token: current_access_token, client_id: client_id)
|
|
58
58
|
new_token = response["accessToken"] || response[:accessToken]
|
|
59
59
|
# Optional: response may include "expiryTime"
|
|
60
60
|
```
|
|
@@ -68,7 +68,7 @@ Example: refresh in provider and cache the result until near expiry:
|
|
|
68
68
|
config.access_token_provider = lambda do
|
|
69
69
|
stored = YourTokenStore.current
|
|
70
70
|
if stored.nil? || stored.expired_soon?
|
|
71
|
-
response = DhanHQ::Auth.renew_token(stored&.access_token || ENV["DHAN_ACCESS_TOKEN"], config.client_id)
|
|
71
|
+
response = DhanHQ::Auth.renew_token(access_token: stored&.access_token || ENV["DHAN_ACCESS_TOKEN"], client_id: config.client_id)
|
|
72
72
|
YourTokenStore.update!(response["accessToken"], response["expiryTime"])
|
|
73
73
|
stored = YourTokenStore.current
|
|
74
74
|
end
|
|
@@ -219,6 +219,4 @@ Central service: configure_from_token_endpoint
|
|
|
219
219
|
|
|
220
220
|
Fully automated: TOTP generate (Auth / Client#generate_access_token)
|
|
221
221
|
|
|
222
|
-
Production-grade automation: enable_auto_token_management! (generate + renew)
|
|
223
|
-
|
|
224
|
-
If you tell me your exact deployment style (single user box, multi-user SaaS, on-prem, etc.), I can tell you which one you should actually use and what to delete as overkill.
|
|
222
|
+
Production-grade automation: enable_auto_token_management! (generate + renew)
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -9,7 +9,7 @@ require 'dhan_hq'
|
|
|
9
9
|
DhanHQ.configure_with_env
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`configure_with_env` reads `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN` from `ENV
|
|
12
|
+
`configure_with_env` reads `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN` from `ENV`. It does **not** raise if either is missing — they're simply left `nil`, and a `DhanHQ::AuthenticationError` (or similar) surfaces later, only when a request actually needs the token.
|
|
13
13
|
|
|
14
14
|
## Required Environment Variables
|
|
15
15
|
|
|
@@ -25,7 +25,8 @@ Set these _before_ calling `configure_with_env` to override defaults:
|
|
|
25
25
|
| Variable | Default | Description |
|
|
26
26
|
| -------------------------------- | ---------- | ------------------------------------------------------- |
|
|
27
27
|
| `DHAN_LOG_LEVEL` | `INFO` | Logger verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`) |
|
|
28
|
-
| `
|
|
28
|
+
| `DHAN_SANDBOX` | `false` | Set `"true"` to route REST calls to `https://sandbox.dhan.co/v2` instead of production. Note: Dhan's sandbox validates request/response plumbing only — it does not execute real order fills/matching. Placed orders stay `PENDING` indefinitely. See [ENDPOINTS_AND_SANDBOX.md](ENDPOINTS_AND_SANDBOX.md). |
|
|
29
|
+
| `DHAN_BASE_URL` | Dhan prod | Point REST calls to a different API hostname. Takes precedence over `DHAN_SANDBOX` only when explicitly set to something other than the production default. |
|
|
29
30
|
| `DHAN_WS_VERSION` | latest | Pin WebSocket connections to a specific API version |
|
|
30
31
|
| `DHAN_WS_ORDER_URL` | Dhan prod | Override the order update WebSocket endpoint |
|
|
31
32
|
| `DHAN_WS_USER_TYPE` | `SELF` | Switch between `SELF` and `PARTNER` streaming modes |
|
data/docs/CONSTANTS_REFERENCE.md
CHANGED
|
@@ -17,8 +17,9 @@ Exchange segments for different markets and instruments.
|
|
|
17
17
|
| `NSE_EQ` | `"NSE_EQ"` | NSE - Equity Cash | 1 |
|
|
18
18
|
| `NSE_FNO` | `"NSE_FNO"` | NSE - Futures & Options | 2 |
|
|
19
19
|
| `NSE_CURRENCY` | `"NSE_CURRENCY"` | NSE - Currency | 3 |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
20
|
+
| `NSE_COMM` | `"NSE_COMM"` | NSE - Commodity | 4 |
|
|
21
|
+
| `BSE_EQ` | `"BSE_EQ"` | BSE - Equity Cash | 5 |
|
|
22
|
+
| `MCX_COMM` | `"MCX_COMM"` | MCX - Commodity | 6 |
|
|
22
23
|
| `BSE_CURRENCY` | `"BSE_CURRENCY"` | BSE - Currency | 7 |
|
|
23
24
|
| `BSE_FNO` | `"BSE_FNO"` | BSE - Futures & Options | 8 |
|
|
24
25
|
|
|
@@ -431,7 +432,7 @@ end
|
|
|
431
432
|
|
|
432
433
|
### Validation Helpers
|
|
433
434
|
|
|
434
|
-
|
|
435
|
+
Already implemented in `lib/DhanHQ/constants.rb`:
|
|
435
436
|
|
|
436
437
|
```ruby
|
|
437
438
|
module DhanHQ
|
|
@@ -439,7 +440,7 @@ module DhanHQ
|
|
|
439
440
|
def self.valid?(module_name, value)
|
|
440
441
|
const_get(module_name)::ALL.include?(value)
|
|
441
442
|
end
|
|
442
|
-
|
|
443
|
+
|
|
443
444
|
def self.all_for(module_name)
|
|
444
445
|
const_get(module_name)::ALL
|
|
445
446
|
end
|
|
@@ -457,10 +458,11 @@ DhanHQ::Constants.all_for(:OrderType) # => ["LIMIT", "MARKET", ...]
|
|
|
457
458
|
|
|
458
459
|
| API Type | Per Second | Per Minute | Per Hour | Per Day |
|
|
459
460
|
|----------|------------|------------|----------|---------|
|
|
460
|
-
| Order APIs | 10 |
|
|
461
|
-
| Data APIs | 5 | - | - |
|
|
461
|
+
| Order APIs | 10 | Unlimited | Unlimited | 100,000 |
|
|
462
|
+
| Data APIs | 5 | - | - | 7,000 |
|
|
462
463
|
| Quote APIs | 1 | Unlimited | Unlimited | Unlimited |
|
|
463
464
|
| Non Trading APIs | 20 | Unlimited | Unlimited | Unlimited |
|
|
465
|
+
| Option Chain | 1 per 3 sec | - | - | - |
|
|
464
466
|
|
|
465
467
|
**Note:** Order modifications are capped at 25 modifications per order.
|
|
466
468
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Sandbox behavior
|
|
4
4
|
|
|
5
|
+
- **Sandbox does NOT execute real order fills/matching.** Confirmed both via Dhan's own docs ("the sandbox behaves like the live API but does not execute real orders") and empirically: a `MARKET` order placed on sandbox validates and returns a real `orderId`, but stays at `PENDING` indefinitely — it never transitions to `TRADED`, even after 24+ seconds of polling. Use sandbox to validate request/response plumbing (auth, payload shape, error codes), not to test fill, cancel, or square-off behavior.
|
|
5
6
|
- **REST:** When `DhanHQ.configuration.sandbox == true` (or `ENV["DHAN_SANDBOX"]=true`), the client uses `https://sandbox.dhan.co/v2` as the base URL for **all** requests that go through `DhanHQ::Client`. Every wrapped REST endpoint listed below is therefore **sent** to the sandbox host when sandbox is enabled.
|
|
6
7
|
- **Sandbox does NOT support WebSocket.** Order updates, market feed, and market depth are **production-only**. The gem always uses production WebSocket URLs regardless of the `sandbox` setting. There are no sandbox WebSocket endpoints in the Dhan v2 API; do not rely on sandbox for real-time streams.
|
|
7
8
|
- **Auth endpoints** (`DhanHQ::Auth`) do **not** use sandbox. They always call:
|
data/docs/LIVE_ORDER_UPDATES.md
CHANGED
|
@@ -320,24 +320,19 @@ This ensures safe usage in multi-threaded applications.
|
|
|
320
320
|
|
|
321
321
|
## Testing
|
|
322
322
|
|
|
323
|
-
For comprehensive testing examples
|
|
323
|
+
For comprehensive testing examples, see the [Testing Guide](TESTING_GUIDE.md). It covers:
|
|
324
324
|
|
|
325
325
|
- **Order Update WebSocket Testing**: Complete examples for all order update features
|
|
326
326
|
- **Event Handler Testing**: Examples for all event types
|
|
327
327
|
- **Order State Management**: Testing order tracking and querying
|
|
328
|
-
- **Interactive Console Helpers**: Load `bin/test_helpers.rb` for quick test functions
|
|
329
328
|
|
|
330
329
|
**Quick start in console:**
|
|
331
330
|
```ruby
|
|
332
331
|
# Start console
|
|
333
332
|
bin/console
|
|
334
333
|
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
test_order_websocket(10) # Test order updates for 10 seconds
|
|
340
|
-
|
|
341
|
-
# Monitor specific order
|
|
342
|
-
monitor_order("112111182045") # Monitor order by ID
|
|
334
|
+
# Test order WebSocket for 10 seconds
|
|
335
|
+
client = DhanHQ::WS::Orders.connect { |update| puts update.inspect }
|
|
336
|
+
sleep 10
|
|
337
|
+
client.stop
|
|
343
338
|
```
|
data/docs/RAILS_INTEGRATION.md
CHANGED
|
@@ -122,7 +122,7 @@ end
|
|
|
122
122
|
|
|
123
123
|
When the API returns 401 or token-expired (error code 807) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. `on_token_expired` is called before that retry so you can refresh your store if needed.
|
|
124
124
|
|
|
125
|
-
**RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token, client_id)` and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
|
|
125
|
+
**RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token: access_token, client_id: client_id)` (keyword arguments) and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
|
|
126
126
|
|
|
127
127
|
## 3. Build service objects for REST flows
|
|
128
128
|
|
data/docs/RELEASE_GUIDE.md
CHANGED
|
@@ -148,8 +148,9 @@ When you push a tag (e.g., `v2.1.12`), the **Release** workflow (`.github/workfl
|
|
|
148
148
|
2. ✅ **Generate OTP code** from secret `RUBYGEMS_OTP_SECRET`
|
|
149
149
|
3. ✅ **Build gem** using `gem build DhanHQ.gemspec`
|
|
150
150
|
4. ✅ **Push to RubyGems** with OTP using `GEM_HOST_API_KEY` (from secret `RUBYGEMS_API_KEY`)
|
|
151
|
+
5. ✅ **Attach gem to GitHub Release** — uploads `DhanHQ-<version>.gem` as a downloadable asset on the GitHub Release for the tag, creating the release (with auto-generated notes) if it doesn't already exist, or uploading to it if it does
|
|
151
152
|
|
|
152
|
-
Tests run on push/PR via the main CI workflow; the release job does not run tests.
|
|
153
|
+
Tests run on push/PR via the main CI workflow; the release job does not run tests. A GitHub Release page is created/updated with the built `.gem` attached as a downloadable asset (verified working on the real v3.1.0 release: https://github.com/shubhamtaywade82/dhanhq-client/releases/tag/v3.1.0).
|
|
153
154
|
|
|
154
155
|
**No manual intervention needed!** ⚡
|
|
155
156
|
|
|
@@ -286,6 +287,16 @@ on:
|
|
|
286
287
|
gem push "DhanHQ-${gem_version}.gem" --otp "$otp_code"
|
|
287
288
|
```
|
|
288
289
|
|
|
290
|
+
5. **Attach gem to GitHub Release**
|
|
291
|
+
```bash
|
|
292
|
+
# Creates the release (with --generate-notes) if it doesn't exist yet, else uploads to it
|
|
293
|
+
if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
294
|
+
gh release upload "$tag" "$gem_file" --repo "$GITHUB_REPOSITORY" --clobber
|
|
295
|
+
else
|
|
296
|
+
gh release create "$tag" "$gem_file" --repo "$GITHUB_REPOSITORY" --title "$tag" --generate-notes
|
|
297
|
+
fi
|
|
298
|
+
```
|
|
299
|
+
|
|
289
300
|
### Security Features
|
|
290
301
|
|
|
291
302
|
- 🔒 **MFA Required** - `rubygems_mfa_required = "true"` in gemspec
|
|
@@ -476,7 +487,7 @@ The old workflow (`.github/workflows/main.yml`):
|
|
|
476
487
|
✅ **MFA secured:** OTP authentication on every release
|
|
477
488
|
✅ **Production ready:** Battle-tested release workflow
|
|
478
489
|
✅ **Tests integrated:** Ensures quality before release
|
|
479
|
-
✅ **Tag-
|
|
490
|
+
✅ **Tag-triggered release:** push tag `v*` to publish to RubyGems and attach the built gem to a GitHub Release
|
|
480
491
|
|
|
481
492
|
Questions or issues? Check the [Troubleshooting](#troubleshooting) section above.
|
|
482
493
|
|
|
@@ -996,8 +996,8 @@ class MarketDataCLI
|
|
|
996
996
|
volume: full[:vol],
|
|
997
997
|
day_high: full[:day_high],
|
|
998
998
|
day_low: full[:day_low],
|
|
999
|
-
open: full[:
|
|
1000
|
-
close: full[:
|
|
999
|
+
open: full[:day_open],
|
|
1000
|
+
close: full[:day_close],
|
|
1001
1001
|
timestamp: timestamp.iso8601
|
|
1002
1002
|
}
|
|
1003
1003
|
|