DhanHQ 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +51 -0
- data/README.md +152 -4
- data/docs/CONSTANTS_REFERENCE.md +3 -2
- data/exe/dhanhq-mcp +7 -0
- data/lib/DhanHQ/agent/tool_registry.rb +51 -2
- data/lib/DhanHQ/concerns/order_audit.rb +43 -1
- data/lib/DhanHQ/constants.rb +3 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
- data/lib/DhanHQ/mcp/server.rb +172 -9
- data/lib/DhanHQ/models/instrument.rb +44 -14
- 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/iceberg_orders.rb +1 -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/dhan_hq.rb +47 -0
- data/skills/dhanhq-ruby/SKILL.md +174 -41
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
- data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
- data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
- data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
- data/skills/dhanhq-ruby/references/error-codes.md +50 -0
- data/skills/dhanhq-ruby/references/funds.md +67 -0
- data/skills/dhanhq-ruby/references/instruments.md +85 -0
- data/skills/dhanhq-ruby/references/live-feed.md +83 -0
- data/skills/dhanhq-ruby/references/market-data.md +119 -0
- data/skills/dhanhq-ruby/references/option-chain.md +71 -0
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
- data/skills/dhanhq-ruby/references/orders.md +200 -6
- data/skills/dhanhq-ruby/references/portfolio.md +93 -0
- data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
- data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
- metadata +39 -3
- data/skills/dhanhq-ruby/references/market_data.md +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa2811b4faf1f45f3ddc18b5f0b1ae03f481aa30fe48f1b1be3b3a928f87c581
|
|
4
|
+
data.tar.gz: cd4829e129d27e6b939bedaac18828f44237674ef5c84135ce9a647c4af37bcc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b8fedf28da427de98161f6d2288230dc6e34a112bbfb56bda85d6373f0d2489cdfa8c2427facf0dc98e729790b4ee2023f534888f1b84989885993e9272545a
|
|
7
|
+
data.tar.gz: 11500ff76de75b2f9b9205e7f5af1210940291fe05b5c53b5f40d4f91e1a8d1c56346e8adb65c1fb2e2a36509b0c80f198f9264d402be8ced0622057fb288d64
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
+
## [3.1.0] - 2026-07-20
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **MCP resources support** — 6 URI-addressable resources (`dhanhq://account/profile`, `funds`, `holdings`, `positions`, `orders`, `dhanhq://market/capabilities`) with `resources/list` and `resources/read`.
|
|
6
|
+
- **MCP prompts support** — 5 pre-built AI prompts (`portfolio_summary`, `market_analysis`, `risk_report`, `order_preview`, `suggest_strategy`) with `prompts/list` and `prompts/get`.
|
|
7
|
+
- **5 new builtin skills** — `covered_call`, `bull_put_spread`, `bear_call_spread`, `protective_put`, `straddle` (10 total).
|
|
8
|
+
- **3 new risk checks** — `PositionLimits` (max 20 concurrent positions), `Concentration` (max 25% per symbol), `MaxLoss` (daily loss limit, default ₹50,000).
|
|
9
|
+
- **Extended risk pipeline** — `DhanHQ::Risk::Pipeline` now includes `DAILY_CHECKS` constant and runs all new checks.
|
|
10
|
+
- **Risk pipeline wired into all order paths** — `Risk::Pipeline.run!` now fires before every order placement via the `OrderAudit` concern, covering Orders, SuperOrders, ForeverOrders, AlertOrders, TwapOrders, IcebergOrders, and PnL Exit. Instrument resolution failures are handled silently so transient lookup issues never block a valid order.
|
|
11
|
+
- **SDK + AI integration docs** — README sections for MCP Server, Skills System, and AI Integration.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `DhanHQ::MCP::Server` now requires `dhan_hq/ai` for prompt generation.
|
|
16
|
+
- CI RuboCop step no longer uses `continue-on-error: true`.
|
|
17
|
+
- Spec path for risk check specs moved to `spec/dhan_hq/risk/checks/`.
|
|
18
|
+
- **`OrderAudit` concern extended** — new `run_risk_checks!(params)` method runs the risk pipeline before order placement, with `trade_type_for` mapping exchange segments to pipeline types. All 7 order resources call it in their `create`/`configure` methods.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Syntax error in MCP server spec (orphaned `if response["error"]` debug lines removed).
|
|
23
|
+
- `[]` method stub pattern in risk check specs (use `receive(:[])` instead of hash double syntax).
|
|
24
|
+
- `MaxLoss` spec test data corrected to trigger the daily loss limit correctly.
|
|
25
|
+
- All RSpec VerifiedDoubles and MultipleExpectations offenses resolved (0 RuboCop offenses).
|
|
26
|
+
- **Position model accessors in risk checks** — replaced `p[:net_quantity]`, `p[:unrealized_profit_loss]`, and `p[:trading_symbol]` hash indexing with real model accessors (`p.net_qty`, `p.unrealized_profit`, `p.trading_symbol`).
|
|
27
|
+
- **`ltp` access in all 9 skills** — `instrument.ltp` returns a Float, not a Hash; removed the `ltp[:ltp]` unwrapping pattern.
|
|
28
|
+
- **`market_analysis` prompt** — resolves symbol to integer security ID via `Instrument.find` before passing to `MarketFeed.quote`.
|
|
29
|
+
- **`OrderAudit#run_risk_checks!` resolved the wrong instrument** — called `Instrument.find(exchange_segment, security_id)`, but `find`'s second argument is a symbol name (e.g. `"RELIANCE"`), not a security ID. Every real order silently failed instrument resolution (`Instrument.find` returned `nil` for a numeric ID), which combined with the surrounding `rescue StandardError; nil` meant **risk checks never ran for any real order placed through any of the 7 order resources**, despite the "wired into all order paths" claim above. Fixed by switching to the new `Instrument.find_by_security_id`. Confirmed live against a real account: `find("NSE_EQ", "2885")` → `nil`; `find_by_security_id("NSE_EQ", "2885")` → resolves RELIANCE correctly.
|
|
30
|
+
- **MCP JSON-RPC compliance** — notifications (requests with no `id`) no longer receive a response; dispatch errors preserve the caller's request `id`; error codes now use the correct `-32700`/`-32601`/`-32602`/`-32603` instead of a single generic `-32000`; `protocolVersion` is negotiated against the client's request instead of hardcoded.
|
|
31
|
+
- **`dhan_place_order` MCP tool bypassed all risk checks** — now resolves the instrument via `find_by_security_id` and runs `Risk::Pipeline.run!` before calling `Order.place`, same as the resource-level fix above, gated by the same live-write policy checks.
|
|
32
|
+
- **Option chain shape mismatch in 8 of 11 skills** — `Instrument#option_chain` returns `{ last_price:, strikes: [{ strike:, call:, put: }] }` (nested Hash), but `iron_condor`, `straddle`, `strangle`, `buy_atm_call`, `covered_call`, `protective_put`, `bull_put_spread`, and `bear_call_spread` were written assuming a flat array of `{ strike:, option_type:, security_id: }` leg-hashes. Every one of these skills raised `NoMethodError`/`TypeError` against the real API; all specs passed anyway because their fixtures invented the same wrong shape. Confirmed broken live, fixed against real NIFTY/RELIANCE option chains (real security IDs verified), all fixtures rewritten to match reality (`spec/support/option_chain_helpers.rb`).
|
|
33
|
+
- **MCP tool call could hang indefinitely** — a rate-limited `tools/call` blocked the single-threaded stdio loop with no error surfaced. Added a 15s timeout (`DhanHQ::MCP::Server#tool_call_timeout`) that returns a structured error instead.
|
|
34
|
+
- **`dhan_skill_*` tool descriptions** showed the raw Ruby class name (e.g. `"DhanHQ::Skills::Builtin::IronCondor"`) instead of anything useful to an LLM client. Added a `description` class macro to `Skills::Base`; all 11 builtin skills now declare a human-readable one-liner. Verified live via `tools/list`.
|
|
35
|
+
- **`Instrument.find`/`.find_by_security_id` could hang for minutes and use gigabytes of RAM** — both built a full `Instrument` model object (each with ~52 `define_singleton_method` calls from `BaseModel#assign_attributes`) for *every row* in the segment before filtering. Confirmed live: a single `find_by_security_id("NSE_EQ", ...)` call ran 98.9% CPU, 4.2GB RSS, and was killed after 3.5 minutes without finishing — `NSE_EQ` has ~219,000 rows. Fixed both methods to scan raw CSV rows and instantiate only the matching one (~9–11s now, down from unbounded). Added dedicated `.find` spec coverage (previously none existed) including a regression guard asserting only one `Instrument` is ever constructed.
|
|
36
|
+
- **`correlation_id` accepted up to 30 characters in 4 order contracts** (`PlaceOrderContract`, `ForeverOrderContract`, `IcebergOrderContract`, `TwapOrderContract`), but Dhan's real API caps it at 25 and rejects the **entire order** with a generic, field-agnostic `DH-905` error if exceeded — giving no indication `correlation_id` was the problem. Confirmed live by bisecting the exact boundary (25 chars: success: 26 chars: `DH-905`) against a real sandbox order. All 4 contracts corrected to `max_size?: 25`.
|
|
37
|
+
- **`square_off_all`/`square_off_position` never actually found any positions to close** — `SquareOffAll#fetch_positions` called `p[:net_quantity]` on a `Position` model (no `[]` method defined), silently rescued to `0`, and filtered out every real position, always reporting zero exits. `SquareOffPosition#find_position` had the same pattern for `exchange_segment`/`trading_symbol`/`security_id`/`net_quantity`, unguarded by any rescue, so it would raise `NoMethodError` the moment a real position existed. Both bugs were invisible until this session, because no test — unit or live — had ever exercised these skills against a real non-empty `Position.all` result; the unit-test doubles stubbed the same wrong `net_quantity` attribute name the buggy code called. Fixed both to use the real model accessors (`p.net_qty`, `p.exchange_segment`, `p.trading_symbol`, `p.security_id`); fixed both specs' doubles to match. Confirmed live: built a Dhan-API-compatible translation adapter in front of `simulators/paper_exchange` (a real Rails order-matching/fills/positions simulator already in this workspace) so a real order could actually fill (Dhan's own sandbox never executes real fills — confirmed via its docs), producing a genuine non-zero position, then ran `dhan_skill_square_off_all` and `dhan_skill_square_off_position` through the full MCP-gated path against it: both correctly found the position and closed it (`net_qty` 4→0 and 2→0 respectively) with a real `SUCCESS` response.
|
|
38
|
+
|
|
39
|
+
### Known Limitations
|
|
40
|
+
|
|
41
|
+
Verified live against a real Dhan account (live-scoped and sandbox), a real independent MCP client, and — for the two paths sandbox cannot simulate (real fills, real position closure) — a purpose-built Dhan-API-compatible adapter in front of `simulators/paper_exchange`'s real order-matching engine.
|
|
42
|
+
|
|
43
|
+
- **Live-tested, all of it, including the full write path**: core REST client, MCP protocol layer, all 11 skills' intent-building, WebSocket streaming, and — end to end — `dhan_place_order`, `dhan_cancel_order`, `dhan_skill_square_off_all`, and `dhan_skill_square_off_position`, each through the actual MCP-gated path (instrument resolution → full risk pipeline → audit log → real order execution). Dhan's sandbox itself only validates request/response plumbing and never executes a real fill (confirmed via Dhan's own docs: "the sandbox behaves like the live API but does not execute real orders") — fills were exercised against `simulators/paper_exchange`'s real matching engine via a throwaway adapter (not shipped, not committed) instead.
|
|
44
|
+
- **`Concentration`/`PositionLimits`/`MaxLoss` risk checks** have only been observed against zero-position, zero-or-low-balance accounts, and against `paper_exchange`'s simulated position (a single small equity position). Their math is unit-tested; behavior against a large, multi-symbol real portfolio has not been observed.
|
|
45
|
+
- **The sandbox environment's instrument/security-ID catalog appears disconnected from the production instrument master** — a real production TCS security ID (`532540`, resolved via `Instrument.find`) was rejected by the sandbox's matching engine (`DH-906: Transaction has Failed`), while an ID from a prior sandbox test (`11536`) validated successfully. Anyone testing against sandbox should resolve security IDs from sandbox-originated data (e.g. prior order history), not the live instrument master.
|
|
46
|
+
- **`release.yml`'s GitHub Release gem-asset upload** has not fired end-to-end — no tag has been pushed since the fix landed.
|
|
47
|
+
- **v3.1.0 is not tagged** — CHANGELOG reflects work in progress on `main`, not a cut release.
|
|
48
|
+
- **Nothing from this session is committed.**
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
1
52
|
## [3.0.0] - 2026-05-19
|
|
2
53
|
|
|
3
54
|
### Breaking Changes
|
data/README.md
CHANGED
|
@@ -421,13 +421,18 @@ Need initializers, service objects, ActionCable wiring, and background workers?
|
|
|
421
421
|
|
|
422
422
|
These scripts are designed around user goals rather than API surfaces:
|
|
423
423
|
|
|
424
|
-
| Example |
|
|
425
|
-
| ------- |
|
|
426
|
-
| [examples/basic_trading_bot.rb](examples/basic_trading_bot.rb) |
|
|
427
|
-
| [examples/
|
|
424
|
+
| Example | What it covers |
|
|
425
|
+
| ------- | --------------- |
|
|
426
|
+
| [examples/basic_trading_bot.rb](examples/basic_trading_bot.rb) | Trading bot scaffold with live-trading guard |
|
|
427
|
+
| [examples/comprehensive_websocket_examples.rb](examples/comprehensive_websocket_examples.rb) | WebSocket mode coverage and timeout handling |
|
|
428
428
|
| [examples/options_watchlist.rb](examples/options_watchlist.rb) | Build a live options watchlist with index quotes and option-chain context |
|
|
429
429
|
| [examples/market_feed_example.rb](examples/market_feed_example.rb) | Subscribe to major market indices over WebSocket |
|
|
430
|
+
| [examples/market_depth_example.rb](examples/market_depth_example.rb) | Market depth streaming example |
|
|
430
431
|
| [examples/live_order_updates.rb](examples/live_order_updates.rb) | Track order lifecycle events in real time |
|
|
432
|
+
| [examples/order_update_example.rb](examples/order_update_example.rb) | Single-session order-update flow |
|
|
433
|
+
| [examples/portfolio_monitor.rb](examples/portfolio_monitor.rb) | Snapshot funds, holdings, and positions for a monitoring script |
|
|
434
|
+
| [examples/trading_fields_example.rb](examples/trading_fields_example.rb) | Dhan order-field mappings and constants example |
|
|
435
|
+
| [examples/instrument_finder_test.rb](examples/instrument_finder_test.rb) | Instrument search/resolution troubleshooting |
|
|
431
436
|
|
|
432
437
|
For search-driven discovery and onboarding content, see:
|
|
433
438
|
|
|
@@ -471,6 +476,149 @@ For search-driven discovery and onboarding content, see:
|
|
|
471
476
|
|
|
472
477
|
---
|
|
473
478
|
|
|
479
|
+
## MCP Server (AI Agent Integration)
|
|
480
|
+
|
|
481
|
+
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.
|
|
482
|
+
|
|
483
|
+
### Quick Start
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
# 1. Configure credentials
|
|
487
|
+
export DHAN_CLIENT_ID="your_client_id"
|
|
488
|
+
export DHAN_ACCESS_TOKEN="your_access_token"
|
|
489
|
+
|
|
490
|
+
# 2. Start the server (stdio)
|
|
491
|
+
bundle exec dhanhq-mcp
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Claude Desktop config (`claude_desktop_config.json`):
|
|
495
|
+
|
|
496
|
+
```json
|
|
497
|
+
{
|
|
498
|
+
"mcpServers": {
|
|
499
|
+
"dhanhq": {
|
|
500
|
+
"command": "bundle",
|
|
501
|
+
"args": ["exec", "dhanhq-mcp"],
|
|
502
|
+
"env": {
|
|
503
|
+
"DHAN_CLIENT_ID": "your_client_id",
|
|
504
|
+
"DHAN_ACCESS_TOKEN": "your_access_token"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### MCP Features
|
|
512
|
+
|
|
513
|
+
| Feature | Description |
|
|
514
|
+
| ------- | ----------- |
|
|
515
|
+
| **Tools** | 12 primitive tools (profile, funds, holdings, positions, order history, order preview/place/cancel, instruments, market feed) + 11 skill-derived tools (`dhan_skill_*` — one per builtin strategy in [Skills System](#skills-system) below) |
|
|
516
|
+
| **Resources** | 6 URI-addressable data endpoints: `dhanhq://account/profile`, `dhanhq://account/funds`, `dhanhq://account/holdings`, `dhanhq://account/positions`, `dhanhq://account/orders`, `dhanhq://market/capabilities` |
|
|
517
|
+
| **Prompts** | 5 pre-built AI prompts: `portfolio_summary`, `market_analysis`, `risk_report`, `order_preview`, `suggest_strategy` |
|
|
518
|
+
|
|
519
|
+
### Security & Policy
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
# Read-only mode (no order placement)
|
|
523
|
+
DhanHQ::Agent::Policy.read_only
|
|
524
|
+
|
|
525
|
+
# Scope-based policy from env (DHANHQ_AGENT_SCOPES)
|
|
526
|
+
DhanHQ::Agent::Policy.from_env
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The policy engine respects `DHANHQ_MCP_ENABLE_WRITES` and `LIVE_TRADING` env vars. Write operations are blocked by default — explicit opt-in required.
|
|
530
|
+
|
|
531
|
+
## Skills System
|
|
532
|
+
|
|
533
|
+
Skills are reusable, composable trading strategies. DhanHQ ships with **11 builtin skills** and a registry for discovery and invocation.
|
|
534
|
+
|
|
535
|
+
### Builtin Skills
|
|
536
|
+
|
|
537
|
+
| Skill | Type | Description |
|
|
538
|
+
| ----- | ---- | ----------- |
|
|
539
|
+
| `buy_atm_call` | Single-leg | Buy ATM call option |
|
|
540
|
+
| `square_off_all` | Action | Square off all open positions |
|
|
541
|
+
| `square_off_position` | Action | Square off a specific position |
|
|
542
|
+
| `iron_condor` | Multi-leg | Sell OTM put + buy further OTM put + sell OTM call + buy further OTM call |
|
|
543
|
+
| `strangle` | Multi-leg | Buy OTM put + buy OTM call |
|
|
544
|
+
| `covered_call` | Multi-leg | Buy equity + sell OTM call |
|
|
545
|
+
| `bull_put_spread` | Multi-leg | Sell OTM put + buy further OTM put |
|
|
546
|
+
| `bear_call_spread` | Multi-leg | Sell OTM call + buy further OTM call |
|
|
547
|
+
| `protective_put` | Multi-leg | Buy equity + buy OTM put |
|
|
548
|
+
| `straddle` | Multi-leg | Buy ATM call + buy ATM put |
|
|
549
|
+
| `market_data_summarizer` | Read-only | Summarize technicals and/or option chain for a symbol |
|
|
550
|
+
|
|
551
|
+
### Using Skills
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# Register all builtin skills
|
|
555
|
+
DhanHQ::Skills::Registry.load_builtins
|
|
556
|
+
|
|
557
|
+
# Find a skill by name
|
|
558
|
+
skill = DhanHQ::Skills::Registry.find("covered_call")
|
|
559
|
+
|
|
560
|
+
# Invoke — returns an intent hash (trade_type, legs, risk metadata)
|
|
561
|
+
result = skill.call(symbol: "RELIANCE", expiry: "2026-06-25")
|
|
562
|
+
# => { intent: { trade_type: "COVERED_CALL", legs: [...], total_premium: ..., break_even: ..., note: "..." } }
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
Skills return intent hashes (not executed trades), keeping a human-in-the-loop safety pattern.
|
|
566
|
+
|
|
567
|
+
## AI Integration
|
|
568
|
+
|
|
569
|
+
DhanHQ provides prompt helpers and risk reporting for LLM-powered trading agents.
|
|
570
|
+
|
|
571
|
+
### Prompt Helpers
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
require 'dhan_hq/ai'
|
|
575
|
+
|
|
576
|
+
# Portfolio summary for AI consumption
|
|
577
|
+
DhanHQ::AI::PromptHelpers.portfolio_summary
|
|
578
|
+
# => "Portfolio Summary\n━━━━━━━━━━━━━\nFunds: ₹1,00,000.00\n..."
|
|
579
|
+
|
|
580
|
+
# Risk report
|
|
581
|
+
DhanHQ::AI::PromptHelpers.risk_report
|
|
582
|
+
# => "🔴 RISK ALERT: 394.6% drawdown..."
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Risk Pipeline
|
|
586
|
+
|
|
587
|
+
```ruby
|
|
588
|
+
# Run pre-trade risk checks
|
|
589
|
+
DhanHQ::Risk::Pipeline.run!(
|
|
590
|
+
instrument: instrument,
|
|
591
|
+
args: { quantity: 25, price: 24_500 },
|
|
592
|
+
type: :fno,
|
|
593
|
+
now: Time.now
|
|
594
|
+
)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Available checks:
|
|
598
|
+
- **TradingPermission** — blocks instruments where trading is disabled (`buy_sell_indicator != "A"`)
|
|
599
|
+
- **AsmGsm** — blocks ASM/GSM restricted instruments
|
|
600
|
+
- **ProductSupport** — validates bracket/cover order support for the instrument
|
|
601
|
+
- **OrderType** — restricts to `MARKET`/`LIMIT` order types
|
|
602
|
+
- **Quantity** — max 10 units / ₹1,00,000 notional (an agent-safety limit, not a general trading cap)
|
|
603
|
+
- **MarketHours** — verifies market is open (9:15 AM–3:30 PM IST)
|
|
604
|
+
- **PositionLimits** — max 20 concurrent open positions
|
|
605
|
+
- **Concentration** — max 25% of available balance in a single symbol
|
|
606
|
+
- **Options** (options only) — index-only, requires stop loss + target + risk-reward
|
|
607
|
+
- **MaxLoss** (daily) — daily loss limit (default ₹50,000)
|
|
608
|
+
|
|
609
|
+
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.
|
|
610
|
+
|
|
611
|
+
### Known Limitations
|
|
612
|
+
|
|
613
|
+
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:
|
|
614
|
+
|
|
615
|
+
- Read path (profile/funds/holdings/positions/orders/instrument lookup/option chains, all 11 skills' intent-building, WebSocket streaming) — live-verified.
|
|
616
|
+
- 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.
|
|
617
|
+
- 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.
|
|
618
|
+
- 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.
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
474
622
|
## Best Practices
|
|
475
623
|
|
|
476
624
|
- Keep `on(:tick)` handlers **non-blocking** — push heavy work to a queue/thread
|
data/docs/CONSTANTS_REFERENCE.md
CHANGED
|
@@ -457,10 +457,11 @@ DhanHQ::Constants.all_for(:OrderType) # => ["LIMIT", "MARKET", ...]
|
|
|
457
457
|
|
|
458
458
|
| API Type | Per Second | Per Minute | Per Hour | Per Day |
|
|
459
459
|
|----------|------------|------------|----------|---------|
|
|
460
|
-
| Order APIs | 10 |
|
|
461
|
-
| Data APIs | 5 | - | - |
|
|
460
|
+
| Order APIs | 10 | Unlimited | Unlimited | 100,000 |
|
|
461
|
+
| Data APIs | 5 | - | - | 7,000 |
|
|
462
462
|
| Quote APIs | 1 | Unlimited | Unlimited | Unlimited |
|
|
463
463
|
| Non Trading APIs | 20 | Unlimited | Unlimited | Unlimited |
|
|
464
|
+
| Option Chain | 1 per 3 sec | - | - | - |
|
|
464
465
|
|
|
465
466
|
**Note:** Order modifications are capped at 25 modifications per order.
|
|
466
467
|
|
data/exe/dhanhq-mcp
ADDED
|
@@ -65,6 +65,10 @@ module DhanHQ
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def build_tools
|
|
68
|
+
(primitive_tools + skill_tools).to_h { |tool_item| [tool_item.name, tool_item] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def primitive_tools
|
|
68
72
|
[
|
|
69
73
|
tool("dhan_profile", "Fetch Dhan profile", "portfolio:read", "read_only",
|
|
70
74
|
object_schema, profile_handler,
|
|
@@ -124,7 +128,31 @@ module DhanHQ
|
|
|
124
128
|
cancel_schema, cancel_order_handler,
|
|
125
129
|
version: "1.0.0",
|
|
126
130
|
output_schema: { type: "object", properties: { order_id: { type: "string" }, status: { type: "string" } } })
|
|
127
|
-
]
|
|
131
|
+
]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Exposes each registered DhanHQ::Skills::Registry strategy as an MCP tool,
|
|
135
|
+
# gated by the risk/scope the skill class declares (see DhanHQ::Skills::Base).
|
|
136
|
+
def skill_tools
|
|
137
|
+
DhanHQ::Skills::Registry.list.map do |skill|
|
|
138
|
+
klass = DhanHQ::Skills::Registry.find(skill[:name])
|
|
139
|
+
tool("dhan_skill_#{skill[:name]}", skill[:description], klass.scope, klass.risk,
|
|
140
|
+
skill_input_schema(skill[:params]),
|
|
141
|
+
->(arguments) { DhanHQ::Skills::Registry.call(skill[:name], arguments) },
|
|
142
|
+
version: "1.0.0")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def skill_input_schema(params)
|
|
147
|
+
properties = params.transform_values do |config|
|
|
148
|
+
{ type: skill_param_type(config[:type]) }.tap { |h| h[:description] = config[:description] if config[:description] }
|
|
149
|
+
end
|
|
150
|
+
required = params.select { |_, config| config[:required] }.keys.map(&:to_s)
|
|
151
|
+
{ type: "object", properties: properties, required: required, additionalProperties: false }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def skill_param_type(type)
|
|
155
|
+
{ string: "string", integer: "integer", number: "number", boolean: "boolean" }.fetch(type.to_sym, "string")
|
|
128
156
|
end
|
|
129
157
|
|
|
130
158
|
# rubocop:disable Metrics/ParameterLists
|
|
@@ -228,7 +256,20 @@ module DhanHQ
|
|
|
228
256
|
|
|
229
257
|
def preview_handler = ->(arguments) { OrderPreview.new(arguments).to_h }
|
|
230
258
|
|
|
231
|
-
def place_order_handler
|
|
259
|
+
def place_order_handler
|
|
260
|
+
lambda do |arguments|
|
|
261
|
+
instrument = DhanHQ::Models::Instrument.find_by_security_id(arguments[:exchange_segment], arguments[:security_id])
|
|
262
|
+
unless instrument
|
|
263
|
+
raise DhanHQ::RiskViolation,
|
|
264
|
+
"Cannot verify risk for unknown instrument: #{arguments[:exchange_segment]}:#{arguments[:security_id]}"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
risk_type = instrument.instrument_type.to_s.start_with?("OPT") ? :options : :equity
|
|
268
|
+
DhanHQ::Risk::Pipeline.run!(instrument: instrument, args: stringify(arguments), type: risk_type)
|
|
269
|
+
|
|
270
|
+
DhanHQ::Models::Order.place(arguments)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
232
273
|
|
|
233
274
|
def cancel_order_handler
|
|
234
275
|
lambda do |arguments|
|
|
@@ -244,6 +285,14 @@ module DhanHQ
|
|
|
244
285
|
else value
|
|
245
286
|
end
|
|
246
287
|
end
|
|
288
|
+
|
|
289
|
+
def stringify(value)
|
|
290
|
+
case value
|
|
291
|
+
when Hash then value.each_with_object({}) { |(key, val), hash| hash[key.to_s] = stringify(val) }
|
|
292
|
+
when Array then value.map { |val| stringify(val) }
|
|
293
|
+
else value
|
|
294
|
+
end
|
|
295
|
+
end
|
|
247
296
|
end
|
|
248
297
|
# rubocop:enable Metrics/ModuleLength
|
|
249
298
|
end
|
|
@@ -4,7 +4,8 @@ require_relative "../utils/network_inspector"
|
|
|
4
4
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
module Concerns
|
|
7
|
-
# Shared behavior for order audit logging
|
|
7
|
+
# Shared behavior for order audit logging, live trading safety, and
|
|
8
|
+
# pre-trade risk checks.
|
|
8
9
|
#
|
|
9
10
|
# Include this module in any Resource that submits, modifies, or cancels
|
|
10
11
|
# orders on the Dhan API. It provides:
|
|
@@ -17,12 +18,18 @@ module DhanHQ
|
|
|
17
18
|
# unless +ENV["LIVE_TRADING"]+ is +"true"+, preventing accidental order
|
|
18
19
|
# placement from development machines.
|
|
19
20
|
#
|
|
21
|
+
# - {#run_risk_checks!}: runs {DhanHQ::Risk::Pipeline} against the order
|
|
22
|
+
# params before execution. Catches unresolvable instruments and other
|
|
23
|
+
# edge cases silently so the risk layer never blocks a valid order due
|
|
24
|
+
# to a transient lookup failure.
|
|
25
|
+
#
|
|
20
26
|
# @example Including in a resource
|
|
21
27
|
# class MyOrders < BaseAPI
|
|
22
28
|
# include DhanHQ::Concerns::OrderAudit
|
|
23
29
|
#
|
|
24
30
|
# def create(params)
|
|
25
31
|
# ensure_live_trading!
|
|
32
|
+
# run_risk_checks!(params)
|
|
26
33
|
# log_order_context("MY_ORDER_ATTEMPT", params)
|
|
27
34
|
# post("", params: params)
|
|
28
35
|
# end
|
|
@@ -39,6 +46,41 @@ module DhanHQ
|
|
|
39
46
|
"Live trading is disabled. Set ENV[\"LIVE_TRADING\"]=\"true\" to enable order placement."
|
|
40
47
|
end
|
|
41
48
|
|
|
49
|
+
# Runs the risk pipeline for the given order params.
|
|
50
|
+
#
|
|
51
|
+
# Extracts +security_id+ and +exchange_segment+ from the params,
|
|
52
|
+
# resolves the instrument, and calls {DhanHQ::Risk::Pipeline.run!}.
|
|
53
|
+
# If the instrument cannot be resolved the check is skipped silently
|
|
54
|
+
# rather than blocking the order.
|
|
55
|
+
def run_risk_checks!(params)
|
|
56
|
+
security_id = extract_param(params, :securityId, :security_id)
|
|
57
|
+
exchange_segment = extract_param(params, :exchangeSegment, :exchange_segment)
|
|
58
|
+
return unless security_id && exchange_segment
|
|
59
|
+
|
|
60
|
+
instrument = DhanHQ::Models::Instrument.find_by_security_id(exchange_segment, security_id)
|
|
61
|
+
return unless instrument
|
|
62
|
+
|
|
63
|
+
DhanHQ::Risk::Pipeline.run!(
|
|
64
|
+
instrument: instrument,
|
|
65
|
+
args: params,
|
|
66
|
+
type: trade_type_for(exchange_segment),
|
|
67
|
+
now: Time.now
|
|
68
|
+
)
|
|
69
|
+
rescue DhanHQ::RiskViolation
|
|
70
|
+
raise
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Maps an exchange segment string to a pipeline trade type.
|
|
76
|
+
def trade_type_for(exchange_segment)
|
|
77
|
+
case exchange_segment.to_s
|
|
78
|
+
when /^NSE_FNO/, /^BSE_FNO/, /^NSE_CURRENCY/, /^BSE_CURRENCY/ then :fno
|
|
79
|
+
when /^MCX/ then :commodity
|
|
80
|
+
else :equity
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
42
84
|
# Emits a structured JSON log line with machine/network/correlation context.
|
|
43
85
|
# Uses WARN level so it appears even when INFO is silenced.
|
|
44
86
|
def log_order_context(event, params = {})
|
data/lib/DhanHQ/constants.rb
CHANGED
|
@@ -381,9 +381,10 @@ module DhanHQ
|
|
|
381
381
|
|
|
382
382
|
# Public rate limits published in DhanHQ API documentation.
|
|
383
383
|
module RateLimit
|
|
384
|
-
ORDER_API = { per_second: 10,
|
|
385
|
-
DATA_API = { per_second: 5, per_day:
|
|
384
|
+
ORDER_API = { per_second: 10, per_day: 100_000 }.freeze
|
|
385
|
+
DATA_API = { per_second: 5, per_day: 7_000 }.freeze
|
|
386
386
|
QUOTE_API = { per_second: 1 }.freeze
|
|
387
|
+
OPTION_CHAIN = { per_second: 1.0 / 3 }.freeze
|
|
387
388
|
NON_TRADING_API = { per_second: 20 }.freeze
|
|
388
389
|
ORDER_MODIFICATIONS_PER_ORDER = 25
|
|
389
390
|
end
|
|
@@ -17,7 +17,7 @@ module DhanHQ
|
|
|
17
17
|
required(:quantity).filled(:integer, gt?: 0)
|
|
18
18
|
required(:price).filled(:float, gt?: 0)
|
|
19
19
|
required(:trigger_price).filled(:float)
|
|
20
|
-
optional(:correlation_id).maybe(:string, max_size?:
|
|
20
|
+
optional(:correlation_id).maybe(:string, max_size?: 25, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
21
21
|
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
22
22
|
optional(:price1).maybe(:float, gt?: 0)
|
|
23
23
|
optional(:trigger_price1).maybe(:float)
|
|
@@ -21,7 +21,7 @@ module DhanHQ
|
|
|
21
21
|
required(:price).filled(:float, gt?: 0)
|
|
22
22
|
required(:iceberg_qty).filled(:integer, gt?: 0)
|
|
23
23
|
required(:disclosed_quantity).filled(:integer, gteq?: 0)
|
|
24
|
-
optional(:correlation_id).maybe(:string, max_size?:
|
|
24
|
+
optional(:correlation_id).maybe(:string, max_size?: 25, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
25
25
|
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
26
26
|
optional(:after_market_order).maybe(:bool)
|
|
27
27
|
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|
|
@@ -17,7 +17,7 @@ module DhanHQ
|
|
|
17
17
|
required(:quantity).filled(:integer, gt?: 0)
|
|
18
18
|
|
|
19
19
|
# Optional fields
|
|
20
|
-
optional(:correlation_id).maybe(:string, max_size?:
|
|
20
|
+
optional(:correlation_id).maybe(:string, max_size?: 25, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
21
21
|
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
22
22
|
optional(:price).maybe(:float, gt?: 0)
|
|
23
23
|
optional(:trigger_price).maybe(:float, gt?: 0)
|
|
@@ -22,7 +22,7 @@ module DhanHQ
|
|
|
22
22
|
required(:slice_interval).filled(:integer, gt?: 0)
|
|
23
23
|
required(:start_time).filled(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
24
24
|
required(:end_time).filled(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
25
|
-
optional(:correlation_id).maybe(:string, max_size?:
|
|
25
|
+
optional(:correlation_id).maybe(:string, max_size?: 25, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
26
26
|
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
27
27
|
optional(:after_market_order).maybe(:bool)
|
|
28
28
|
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|