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
|
@@ -317,12 +317,10 @@ end
|
|
|
317
317
|
client = DhanHQ::WS::MarketDepth.client
|
|
318
318
|
|
|
319
319
|
# Event handlers
|
|
320
|
+
# Note: the current implementation only ever emits :depth_update (never :depth_snapshot),
|
|
321
|
+
# and update_data has no :side key — inspect update_data[:bids]/[:asks] directly instead.
|
|
320
322
|
client.on(:depth_update) do |update_data|
|
|
321
|
-
puts "📊 Depth Update: #{update_data[:symbol]} -
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
client.on(:depth_snapshot) do |snapshot_data|
|
|
325
|
-
puts "📸 Depth Snapshot: #{snapshot_data[:symbol]} - Full order book received"
|
|
323
|
+
puts "📊 Depth Update: #{update_data[:symbol]} - bids/asks refreshed"
|
|
326
324
|
end
|
|
327
325
|
|
|
328
326
|
client.on(:error) do |error|
|
|
@@ -771,8 +769,7 @@ puts "Orders connected: #{orders_client.connected?}"
|
|
|
771
769
|
puts "Market connected: #{market_client.connected?}"
|
|
772
770
|
puts "Depth connected: #{depth_client.connected?}"
|
|
773
771
|
|
|
774
|
-
# Get subscription info
|
|
775
|
-
puts "Market subscriptions: #{market_client.subscriptions}"
|
|
772
|
+
# Get subscription info (only DhanHQ::WS::MarketDepth::Client exposes this)
|
|
776
773
|
puts "Depth subscriptions: #{depth_client.subscriptions}"
|
|
777
774
|
```
|
|
778
775
|
|
|
@@ -890,30 +887,26 @@ puts "Orders connected: #{orders_client.connected?}"
|
|
|
890
887
|
puts "Market connected: #{market_client.connected?}"
|
|
891
888
|
puts "Depth connected: #{depth_client.connected?}"
|
|
892
889
|
|
|
893
|
-
# Get subscription info
|
|
894
|
-
puts "Market subscriptions: #{market_client.subscriptions}"
|
|
890
|
+
# Get subscription info (only DhanHQ::WS::MarketDepth::Client exposes this)
|
|
895
891
|
puts "Depth subscriptions: #{depth_client.subscriptions}"
|
|
896
892
|
```
|
|
897
893
|
|
|
898
894
|
## Testing
|
|
899
895
|
|
|
900
|
-
For comprehensive testing examples
|
|
901
|
-
|
|
902
|
-
- **WebSocket Testing**: Market feed, order updates, and market depth examples
|
|
903
|
-
- **Interactive Console Helpers**: Load `bin/test_helpers.rb` for quick test functions
|
|
904
|
-
- **Complete Examples**: Copy-paste examples for all WebSocket features
|
|
896
|
+
For comprehensive testing examples, see the [Testing Guide](TESTING_GUIDE.md).
|
|
905
897
|
|
|
906
898
|
**Quick start in console:**
|
|
907
899
|
```ruby
|
|
908
900
|
# Start console
|
|
909
901
|
bin/console
|
|
910
902
|
|
|
911
|
-
#
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
903
|
+
# Test market feed for 5 seconds
|
|
904
|
+
ticks = []
|
|
905
|
+
client = DhanHQ::WS.connect(mode: :ticker) { |tick| ticks << tick }
|
|
906
|
+
client.subscribe_one(segment: "IDX_I", security_id: 13)
|
|
907
|
+
sleep 5
|
|
908
|
+
client.disconnect!
|
|
909
|
+
p ticks
|
|
917
910
|
```
|
|
918
911
|
|
|
919
912
|
## Examples
|
data/docs/WEBSOCKET_PROTOCOL.md
CHANGED
|
@@ -16,14 +16,16 @@ Low-level protocol details for the DhanHQ WebSocket market feed. For high-level
|
|
|
16
16
|
|
|
17
17
|
## Request Codes
|
|
18
18
|
|
|
19
|
-
Per
|
|
19
|
+
Per the actual implementation (`lib/DhanHQ/ws/connection.rb`):
|
|
20
20
|
|
|
21
21
|
| Action | Ticker | Quote | Full |
|
|
22
22
|
| ------------ | ------ | ----- | ---- |
|
|
23
|
-
| Subscribe | 15 |
|
|
24
|
-
| Unsubscribe |
|
|
23
|
+
| Subscribe | 15 | 15 | 15 |
|
|
24
|
+
| Unsubscribe | 12 | 12 | 12 |
|
|
25
25
|
| Disconnect | 12 | 12 | 12 |
|
|
26
26
|
|
|
27
|
+
All three modes send request code `15` to subscribe (per the official API — mode is set at connection time via the WebSocket URL, not per-message). "Unsubscribe" reuses the disconnect code `12` for all modes rather than distinct per-mode codes.
|
|
28
|
+
|
|
27
29
|
---
|
|
28
30
|
|
|
29
31
|
## Packet Parsing
|
|
@@ -48,7 +50,9 @@ Per Dhan documentation:
|
|
|
48
50
|
| 6 | Prev Close | `prev_close`, `oi_prev` |
|
|
49
51
|
| 7 | Market Status | Raw/misc unless documented |
|
|
50
52
|
| 8 | Full | Quote fields + `open_interest` + 5× depth (bid/ask) |
|
|
53
|
+
| 41 | Depth Bid | Market depth bid levels (20-level depth feed) |
|
|
51
54
|
| 50 | Disconnect | Reason code |
|
|
55
|
+
| 51 | Depth Ask | Market depth ask levels (20-level depth feed) |
|
|
52
56
|
|
|
53
57
|
---
|
|
54
58
|
|
data/exe/dhanhq-mcp
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# Recursive key coercion for arguments crossing the agent boundary.
|
|
6
|
+
#
|
|
7
|
+
# MCP delivers arguments as JSON, so keys arrive as strings; handlers and the risk
|
|
8
|
+
# pipeline expect symbols and strings respectively.
|
|
9
|
+
#
|
|
10
|
+
# Called explicitly (`KeyCoercion.symbolize(...)`) rather than mixed in, so there is
|
|
11
|
+
# no question about method visibility at the call site.
|
|
12
|
+
module KeyCoercion
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param value [Hash, Array, Object]
|
|
16
|
+
# @return [Hash, Array, Object] Same shape with symbol keys throughout.
|
|
17
|
+
def symbolize(value)
|
|
18
|
+
case value
|
|
19
|
+
when Hash then value.each_with_object({}) { |(key, val), hash| hash[key.to_sym] = symbolize(val) }
|
|
20
|
+
when Array then value.map { |val| symbolize(val) }
|
|
21
|
+
else value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param value [Hash, Array, Object]
|
|
26
|
+
# @return [Hash, Array, Object] Same shape with string keys throughout.
|
|
27
|
+
def stringify(value)
|
|
28
|
+
case value
|
|
29
|
+
when Hash then value.each_with_object({}) { |(key, val), hash| hash[key.to_s] = stringify(val) }
|
|
30
|
+
when Array then value.map { |val| stringify(val) }
|
|
31
|
+
else value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# A single tool exposed to MCP clients and agent skills.
|
|
6
|
+
#
|
|
7
|
+
# - +name+, +description+ — what a client sees in `tools/list`
|
|
8
|
+
# - +scope+ — the {DhanHQ::Agent::Policy} scope required to call it
|
|
9
|
+
# - +risk+ — +read_only+, +trade_adjacent_read+, +live_write+ or +destructive_write+;
|
|
10
|
+
# anything ending in +write+ goes through `Policy#require_write!`
|
|
11
|
+
# - +schema+, +output_schema+ — JSON Schema for input and return value
|
|
12
|
+
# - +handler+ — callable invoked with symbolized arguments
|
|
13
|
+
# - +version+ — semantic version of this tool definition
|
|
14
|
+
# - +examples+ — optional input/output pairs shown to clients
|
|
15
|
+
Tool = Struct.new(:name, :description, :scope, :risk, :schema, :handler,
|
|
16
|
+
:version, :output_schema, :examples) do
|
|
17
|
+
# @return [Hash] Client-facing metadata, omitting unset optional fields.
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
name: name,
|
|
21
|
+
description: description,
|
|
22
|
+
scope: scope,
|
|
23
|
+
risk: risk,
|
|
24
|
+
input_schema: schema,
|
|
25
|
+
output_schema: output_schema,
|
|
26
|
+
version: version,
|
|
27
|
+
examples: examples
|
|
28
|
+
}.compact
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] True when calling this tool changes account state.
|
|
32
|
+
def write?
|
|
33
|
+
risk.to_s.end_with?("write")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# The catalogue of tools exposed to MCP clients and agent skills: what exists, at
|
|
6
|
+
# what scope, at what risk, and which handler carries it out.
|
|
7
|
+
#
|
|
8
|
+
# Declarative on purpose — adding an endpoint means adding an entry here, a schema
|
|
9
|
+
# in {DhanHQ::Agent::ToolSchemas} and a handler in {DhanHQ::Agent::ToolHandlers}.
|
|
10
|
+
# {DhanHQ::Agent::ToolRegistry} consumes this and is left responsible only for
|
|
11
|
+
# lookup, policy enforcement and dispatch.
|
|
12
|
+
module ToolCatalogue
|
|
13
|
+
extend ToolSchemas
|
|
14
|
+
extend ToolHandlers
|
|
15
|
+
|
|
16
|
+
# Version stamped on a tool definition that does not declare its own.
|
|
17
|
+
DEFAULT_TOOL_VERSION = "1.0.0"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Every tool, keyed by name.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash{String => DhanHQ::Agent::Tool}]
|
|
24
|
+
def all
|
|
25
|
+
build_tools
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_tools
|
|
29
|
+
(primitive_tools + global_stocks_tools + skill_tools).to_h { |tool_item| [tool_item.name, tool_item] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def primitive_tools
|
|
33
|
+
[
|
|
34
|
+
tool(name: "dhan_profile", description: "Fetch Dhan profile",
|
|
35
|
+
scope: "portfolio:read", risk: "read_only",
|
|
36
|
+
schema: object_schema, handler: profile_handler,
|
|
37
|
+
output_schema: { type: "object", properties: { client_id: { type: "string" } } }),
|
|
38
|
+
tool(name: "dhan_funds", description: "Fetch fund limits",
|
|
39
|
+
scope: "portfolio:read", risk: "read_only",
|
|
40
|
+
schema: object_schema, handler: funds_handler,
|
|
41
|
+
output_schema: { type: "object", properties: { available_balance: { type: "number" } } }),
|
|
42
|
+
tool(name: "dhan_holdings", description: "List holdings",
|
|
43
|
+
scope: "portfolio:read", risk: "read_only",
|
|
44
|
+
schema: object_schema, handler: holdings_handler,
|
|
45
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
46
|
+
tool(name: "dhan_positions", description: "List positions",
|
|
47
|
+
scope: "portfolio:read", risk: "read_only",
|
|
48
|
+
schema: object_schema, handler: positions_handler,
|
|
49
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
50
|
+
tool(name: "dhan_orders", description: "List orders",
|
|
51
|
+
scope: "orders:read", risk: "read_only",
|
|
52
|
+
schema: object_schema, handler: orders_handler,
|
|
53
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
54
|
+
tool(name: "dhan_trades", description: "List trades",
|
|
55
|
+
scope: "orders:read", risk: "read_only",
|
|
56
|
+
schema: object_schema, handler: trades_handler,
|
|
57
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
58
|
+
tool(name: "dhan_search_instruments", description: "Resolve symbols to security IDs",
|
|
59
|
+
scope: "market:read", risk: "read_only",
|
|
60
|
+
schema: search_schema, handler: search_handler,
|
|
61
|
+
output_schema: { type: "array", items: { type: "object" } },
|
|
62
|
+
examples: [
|
|
63
|
+
{ input: { query: "RELIANCE" }, output: "[{security_id: '2885', symbol_name: 'RELIANCE'}]" }
|
|
64
|
+
]),
|
|
65
|
+
tool(name: "dhan_ltp", description: "Fetch last traded prices",
|
|
66
|
+
scope: "market:read", risk: "read_only",
|
|
67
|
+
schema: feed_schema, handler: ltp_handler,
|
|
68
|
+
output_schema: { type: "object", additionalProperties: { type: "number" } }),
|
|
69
|
+
tool(name: "dhan_quote", description: "Fetch market quotes",
|
|
70
|
+
scope: "market:read", risk: "read_only",
|
|
71
|
+
schema: feed_schema, handler: quote_handler,
|
|
72
|
+
output_schema: { type: "object", additionalProperties: { type: "object" } }),
|
|
73
|
+
tool(name: "dhan_order_preview", description: "Validate and summarize an order without placing it",
|
|
74
|
+
scope: "orders:read", risk: "trade_adjacent_read",
|
|
75
|
+
schema: order_schema, handler: preview_handler,
|
|
76
|
+
output_schema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
valid: { type: "boolean" },
|
|
80
|
+
errors: { type: "array" },
|
|
81
|
+
summary: { type: "string" }
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
tool(name: "dhan_place_order", description: "Place an order after external confirmation",
|
|
85
|
+
scope: "orders:write", risk: "live_write",
|
|
86
|
+
schema: order_schema, handler: place_order_handler,
|
|
87
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" } } }),
|
|
88
|
+
tool(name: "dhan_cancel_order", description: "Cancel an order",
|
|
89
|
+
scope: "orders:cancel", risk: "destructive_write",
|
|
90
|
+
schema: cancel_schema, handler: cancel_order_handler,
|
|
91
|
+
output_schema: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: { order_id: { type: "string" }, status: { type: "string" } }
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Tools for the Global Stocks (US equities) book, plus basket orders.
|
|
99
|
+
#
|
|
100
|
+
# Global Stocks are a separate book from domestic NSE/BSE trading, so they get
|
|
101
|
+
# their own tools rather than extra flags on the domestic ones — an agent asked
|
|
102
|
+
# for "my holdings" should not silently mix INR and USD positions.
|
|
103
|
+
def global_stocks_tools
|
|
104
|
+
[
|
|
105
|
+
tool(name: "dhan_global_holdings", description: "List US stock holdings",
|
|
106
|
+
scope: "portfolio:read", risk: "read_only",
|
|
107
|
+
schema: object_schema, handler: global_holdings_handler,
|
|
108
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
109
|
+
tool(name: "dhan_global_funds", description: "Fetch US (USD) fund limits",
|
|
110
|
+
scope: "portfolio:read", risk: "read_only",
|
|
111
|
+
schema: object_schema, handler: global_funds_handler,
|
|
112
|
+
output_schema: { type: "object", properties: { available_cash: { type: "number" } } }),
|
|
113
|
+
tool(name: "dhan_global_orders", description: "List US stock orders",
|
|
114
|
+
scope: "orders:read", risk: "read_only",
|
|
115
|
+
schema: object_schema, handler: global_orders_handler,
|
|
116
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
117
|
+
tool(name: "dhan_global_trades", description: "List US stock trades",
|
|
118
|
+
scope: "orders:read", risk: "read_only",
|
|
119
|
+
schema: object_schema, handler: global_trades_handler,
|
|
120
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
121
|
+
tool(name: "dhan_global_market_status", description: "Check whether the US market is open",
|
|
122
|
+
scope: "market:read", risk: "read_only",
|
|
123
|
+
schema: object_schema, handler: global_market_status_handler,
|
|
124
|
+
output_schema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: { status: { type: "string" }, open: { type: "boolean" } }
|
|
127
|
+
}),
|
|
128
|
+
tool(name: "dhan_global_order_estimate",
|
|
129
|
+
description: "Estimate charges and margin for a US stock order without placing it",
|
|
130
|
+
scope: "orders:read", risk: "trade_adjacent_read",
|
|
131
|
+
schema: global_estimate_schema, handler: global_estimate_handler,
|
|
132
|
+
output_schema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: { total_charges: { type: "number" }, total_margin: { type: "number" } }
|
|
135
|
+
}),
|
|
136
|
+
tool(name: "dhan_global_place_order", description: "Place a US stock order after external confirmation",
|
|
137
|
+
scope: "orders:write", risk: "live_write",
|
|
138
|
+
schema: global_order_schema, handler: global_place_order_handler,
|
|
139
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" } } }),
|
|
140
|
+
tool(name: "dhan_global_cancel_order", description: "Cancel a US stock order",
|
|
141
|
+
scope: "orders:cancel", risk: "destructive_write",
|
|
142
|
+
schema: cancel_schema, handler: global_cancel_order_handler,
|
|
143
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" } } }),
|
|
144
|
+
tool(name: "dhan_multi_order", description: "Place a basket of up to 15 domestic orders in one request",
|
|
145
|
+
scope: "orders:write", risk: "live_write",
|
|
146
|
+
schema: multi_order_schema, handler: multi_order_handler,
|
|
147
|
+
output_schema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: { orders: { type: "array", items: { type: "object" } } }
|
|
150
|
+
})
|
|
151
|
+
]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Exposes each registered DhanHQ::Skills::Registry strategy as an MCP tool,
|
|
155
|
+
# gated by the risk/scope the skill class declares (see DhanHQ::Skills::Base).
|
|
156
|
+
def skill_tools
|
|
157
|
+
DhanHQ::Skills::Registry.list.map do |skill|
|
|
158
|
+
klass = DhanHQ::Skills::Registry.find(skill[:name])
|
|
159
|
+
tool(name: "dhan_skill_#{skill[:name]}", description: skill[:description],
|
|
160
|
+
scope: klass.scope, risk: klass.risk,
|
|
161
|
+
schema: skill_input_schema(skill[:params]),
|
|
162
|
+
handler: ->(arguments) { DhanHQ::Skills::Registry.call(skill[:name], arguments) })
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Builds a {DhanHQ::Agent::Tool}.
|
|
167
|
+
#
|
|
168
|
+
# Keyword-only: with nine attributes, positional order was a memory test, and a
|
|
169
|
+
# transposed scope and risk would have produced a silently mis-gated tool.
|
|
170
|
+
# An unknown attribute raises, so a typo cannot create a half-built tool.
|
|
171
|
+
#
|
|
172
|
+
# @param attributes [Hash] Any {DhanHQ::Agent::Tool} member. +version+ defaults
|
|
173
|
+
# to {DEFAULT_TOOL_VERSION}.
|
|
174
|
+
# @return [DhanHQ::Agent::Tool]
|
|
175
|
+
def tool(**attributes)
|
|
176
|
+
Tool.new(version: DEFAULT_TOOL_VERSION, **attributes)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# Callables that carry out each agent tool, one per tool.
|
|
6
|
+
#
|
|
7
|
+
# Every handler takes a hash of symbolized arguments and returns a model, a plain
|
|
8
|
+
# hash, or a boolean. Policy enforcement happens before a handler is reached — see
|
|
9
|
+
# {DhanHQ::Agent::ToolRegistry#execute} — so handlers here contain no scope or
|
|
10
|
+
# live-trading checks of their own; the resources they call apply the
|
|
11
|
+
# `LIVE_TRADING` gate and audit logging.
|
|
12
|
+
module ToolHandlers
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def profile_handler = ->(_) { DhanHQ::Models::Profile.fetch }
|
|
16
|
+
|
|
17
|
+
def funds_handler = ->(_) { DhanHQ::Models::Funds.fetch }
|
|
18
|
+
|
|
19
|
+
def holdings_handler = ->(_) { DhanHQ::Models::Holding.all }
|
|
20
|
+
|
|
21
|
+
def positions_handler = ->(_) { DhanHQ::Models::Position.all }
|
|
22
|
+
|
|
23
|
+
def orders_handler = ->(_) { DhanHQ::Models::Order.all }
|
|
24
|
+
|
|
25
|
+
def trades_handler = ->(_) { DhanHQ::Models::Trade.today }
|
|
26
|
+
|
|
27
|
+
def search_handler
|
|
28
|
+
lambda do |arguments|
|
|
29
|
+
query = arguments.fetch(:query)
|
|
30
|
+
options = arguments.except(:query)
|
|
31
|
+
DhanHQ::Models::Instrument.search(query, **options)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ltp_handler = ->(arguments) { DhanHQ::Models::MarketFeed.ltp(arguments[:instruments]) }
|
|
36
|
+
|
|
37
|
+
def quote_handler = ->(arguments) { DhanHQ::Models::MarketFeed.quote(arguments[:instruments]) }
|
|
38
|
+
|
|
39
|
+
def preview_handler = ->(arguments) { OrderPreview.new(arguments).to_h }
|
|
40
|
+
|
|
41
|
+
def place_order_handler
|
|
42
|
+
lambda do |arguments|
|
|
43
|
+
instrument = DhanHQ::Models::Instrument.find_by_security_id(arguments[:exchange_segment], arguments[:security_id])
|
|
44
|
+
unless instrument
|
|
45
|
+
raise DhanHQ::RiskViolation,
|
|
46
|
+
"Cannot verify risk for unknown instrument: #{arguments[:exchange_segment]}:#{arguments[:security_id]}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
risk_type = instrument.instrument_type.to_s.start_with?("OPT") ? :options : :equity
|
|
50
|
+
DhanHQ::Risk::Pipeline.run!(instrument: instrument, args: KeyCoercion.stringify(arguments), type: risk_type)
|
|
51
|
+
|
|
52
|
+
DhanHQ::Models::Order.place(arguments)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cancel_order_handler
|
|
57
|
+
lambda do |arguments|
|
|
58
|
+
order = DhanHQ::Models::Order.find(arguments[:order_id])
|
|
59
|
+
order&.cancel || false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def global_holdings_handler = ->(_) { DhanHQ::Models::GlobalStocks::Holding.all }
|
|
64
|
+
|
|
65
|
+
def global_funds_handler = ->(_) { DhanHQ::Models::GlobalStocks::Funds.fetch }
|
|
66
|
+
|
|
67
|
+
def global_orders_handler = ->(_) { DhanHQ::Models::GlobalStocks::Order.all }
|
|
68
|
+
|
|
69
|
+
def global_trades_handler = ->(_) { DhanHQ::Models::GlobalStocks::Trade.all }
|
|
70
|
+
|
|
71
|
+
def global_market_status_handler
|
|
72
|
+
lambda do |_|
|
|
73
|
+
status = DhanHQ::Models::GlobalStocks::MarketStatus.fetch
|
|
74
|
+
{
|
|
75
|
+
status: status.status,
|
|
76
|
+
open: status.open?,
|
|
77
|
+
holiday: status.holiday?,
|
|
78
|
+
market_open_time: status.market_open_time,
|
|
79
|
+
market_close_time: status.market_close_time
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Combines the charge estimate and the margin requirement so an agent can decide
|
|
85
|
+
# affordability in one call rather than two.
|
|
86
|
+
def global_estimate_handler
|
|
87
|
+
lambda do |arguments|
|
|
88
|
+
estimate = DhanHQ::Models::GlobalStocks::OrderEstimate.calculate(arguments)
|
|
89
|
+
margin = DhanHQ::Models::GlobalStocks::Margin.calculate(arguments)
|
|
90
|
+
{
|
|
91
|
+
total_charges: estimate.total_charges,
|
|
92
|
+
brokerage: estimate.brokerage,
|
|
93
|
+
total_margin: margin.total_margin,
|
|
94
|
+
available_balance: margin.available_bal,
|
|
95
|
+
sufficient: margin.sufficient?
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# No {DhanHQ::Risk::Pipeline} run here: its checks resolve instruments from the
|
|
101
|
+
# Indian scrip master and encode NSE/BSE rules, neither of which applies to US
|
|
102
|
+
# equities. The LIVE_TRADING gate and audit logging in the resource still apply,
|
|
103
|
+
# as does {Policy#require_write!}.
|
|
104
|
+
def global_place_order_handler = ->(arguments) { DhanHQ::Models::GlobalStocks::Order.place(arguments) }
|
|
105
|
+
|
|
106
|
+
def global_cancel_order_handler
|
|
107
|
+
lambda do |arguments|
|
|
108
|
+
order_id = arguments[:order_id]
|
|
109
|
+
{ order_id: order_id, cancelled: DhanHQ::Models::GlobalStocks::Order.cancel(order_id) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def multi_order_handler = ->(arguments) { DhanHQ::Models::MultiOrder.place(arguments[:orders]) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|