DhanHQ 2.8.0 → 3.0.1
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_todo.yml +7 -0
- data/CHANGELOG.md +19 -0
- data/README.md +16 -5
- data/docs/RAILS_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/TECHNICAL_ANALYSIS.md +1 -1
- data/exe/DhanHQ +1 -1
- data/lib/DhanHQ/agent/order_preview.rb +50 -0
- data/lib/DhanHQ/agent/policy.rb +51 -0
- data/lib/DhanHQ/agent/tool_registry.rb +250 -0
- data/lib/DhanHQ/agent.rb +12 -0
- data/lib/DhanHQ/ai/context_builder.rb +145 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
- data/lib/DhanHQ/ai.rb +27 -0
- data/lib/DhanHQ/auth.rb +0 -1
- data/lib/DhanHQ/client.rb +1 -3
- data/lib/DhanHQ/constants.rb +2 -0
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
- data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
- data/lib/DhanHQ/core/auth_api.rb +0 -1
- data/lib/DhanHQ/errors.rb +4 -0
- data/lib/DhanHQ/events/base.rb +203 -0
- data/lib/DhanHQ/events/bus.rb +158 -0
- data/lib/DhanHQ/events.rb +40 -0
- data/lib/DhanHQ/indicators.rb +283 -0
- data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
- data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
- data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
- data/lib/DhanHQ/market_data.rb +25 -0
- data/lib/DhanHQ/mcp/server.rb +72 -0
- data/lib/DhanHQ/mcp.rb +10 -0
- data/lib/DhanHQ/models/funds.rb +12 -0
- data/lib/DhanHQ/models/holding.rb +42 -0
- data/lib/DhanHQ/models/iceberg_order.rb +139 -0
- data/lib/DhanHQ/models/instrument.rb +36 -0
- data/lib/DhanHQ/models/order.rb +95 -0
- data/lib/DhanHQ/models/position.rb +66 -0
- data/lib/DhanHQ/models/search_result.rb +12 -0
- data/lib/DhanHQ/models/trade.rb +13 -0
- data/lib/DhanHQ/models/twap_order.rb +136 -0
- data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
- data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
- data/lib/DhanHQ/option_analytics.rb +36 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +61 -0
- data/lib/DhanHQ/resources/twap_orders.rb +61 -0
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
- data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
- data/lib/DhanHQ/risk/checks/options.rb +46 -0
- data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
- data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
- data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
- data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
- data/lib/DhanHQ/risk/pipeline.rb +65 -0
- data/lib/DhanHQ/risk.rb +250 -0
- data/lib/DhanHQ/skills/base.rb +132 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +87 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +93 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +45 -0
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +48 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
- data/lib/DhanHQ/skills/registry.rb +101 -0
- data/lib/DhanHQ/skills/workflow.rb +66 -0
- data/lib/DhanHQ/skills.rb +29 -0
- data/lib/DhanHQ/strategy/base.rb +189 -0
- data/lib/DhanHQ/strategy.rb +40 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/decoder.rb +57 -19
- data/lib/DhanHQ.rb +3 -0
- data/lib/dhan_hq/agent.rb +3 -0
- data/lib/dhan_hq/analysis.rb +9 -0
- data/lib/dhan_hq/mcp.rb +3 -0
- data/lib/dhan_hq/ta.rb +5 -0
- data/lib/dhan_hq.rb +27 -4
- data/lib/ta/technical_analysis.rb +3 -1
- data/skills/dhanhq-ruby/SKILL.md +74 -0
- data/skills/dhanhq-ruby/references/market_data.md +3 -0
- data/skills/dhanhq-ruby/references/orders.md +7 -0
- metadata +61 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d9d117dd455fc2a006a98cf0532297688a247498f5a12e98fb609a355ffc195
|
|
4
|
+
data.tar.gz: 2334b0e16f35ecc00f414739f3d40da1cac7640ede1b64079248b35e02cc0851
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 577d961c10faa0381d7e4f17a835688c181457bc7ed2222755123dd4cb7735431c1652af5d7026420c977b330863d9576354cc65c44aa1d7bc9342c1e759cce3
|
|
7
|
+
data.tar.gz: fc6ba43d2e0e37d8e87c19aa7b803330377aa7c38bff4f90c451192d410a94a9864ea887f90b8a5419c608e0ca57fcb8711bd462976568772318aee29abbcb45
|
data/.rubocop_todo.yml
CHANGED
|
@@ -90,8 +90,10 @@ Naming/MethodParameterName:
|
|
|
90
90
|
Naming/PredicateMethod:
|
|
91
91
|
Exclude:
|
|
92
92
|
- 'lib/DhanHQ/models/forever_order.rb'
|
|
93
|
+
- 'lib/DhanHQ/models/iceberg_order.rb'
|
|
93
94
|
- 'lib/DhanHQ/models/order.rb'
|
|
94
95
|
- 'lib/DhanHQ/models/super_order.rb'
|
|
96
|
+
- 'lib/DhanHQ/models/twap_order.rb'
|
|
95
97
|
- 'lib/DhanHQ/ws/singleton_lock.rb'
|
|
96
98
|
|
|
97
99
|
# Offense count: 13
|
|
@@ -105,6 +107,11 @@ Naming/VariableNumber:
|
|
|
105
107
|
- 'lib/DhanHQ/ws/market_depth/client.rb'
|
|
106
108
|
- 'lib/DhanHQ/ws/orders/connection.rb'
|
|
107
109
|
|
|
110
|
+
# Offense count: 1
|
|
111
|
+
Naming/FileName:
|
|
112
|
+
Exclude:
|
|
113
|
+
- 'lib/DhanHQ.rb'
|
|
114
|
+
|
|
108
115
|
# Offense count: 4
|
|
109
116
|
# Configuration parameters: MinSize.
|
|
110
117
|
Performance/CollectionLiteralInLoop:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [3.0.0] - 2026-05-19
|
|
2
|
+
|
|
3
|
+
### Breaking Changes
|
|
4
|
+
|
|
5
|
+
- **TA and analysis modules are now opt-in**. `require 'dhan_hq'` no longer auto-loads `DhanHQ::Analysis::*` or `TA::*`. Explicitly require what you need:
|
|
6
|
+
```ruby
|
|
7
|
+
require 'dhan_hq/analysis' # OptionsBuyingAdvisor, MultiTimeframeAnalyzer
|
|
8
|
+
require 'dhan_hq/ta' # TA::TechnicalAnalysis, TA::Fetcher, TA::Candles
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `exe/DhanHQ` now correctly uses `require "dhan_hq"` (was the old `require "DhanHQ"`).
|
|
14
|
+
- Gemspec file renamed back to `DhanHQ.gemspec` (was `dhan_hq.gemspec`) to resolve RubyGems "too similar" name collision and ensure successful push of the `DhanHQ` gem.
|
|
15
|
+
- Updated all `Gemfile` references and installation docs to use `DhanHQ` while maintaining `require 'dhan_hq'` for standard Ruby conventions.
|
|
16
|
+
- SimpleCov now tracks all `lib/**/*.rb` files (was models-only).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
1
20
|
## [2.8.0] - 2026-03-21
|
|
2
21
|
|
|
3
22
|
### Added
|
data/README.md
CHANGED
|
@@ -153,16 +153,23 @@ gem install DhanHQ
|
|
|
153
153
|
|
|
154
154
|
**`bundle update` / `bundle install` warnings** — If you see "Local specification for rexml-3.2.8 has different dependencies" or "Unresolved or ambiguous specs during Gem::Specification.reset: psych", the bundle still completes successfully. To clear the rexml warning once, run: `gem cleanup rexml`. The psych message is a known Bundler quirk and can be ignored.
|
|
155
155
|
|
|
156
|
-
###
|
|
156
|
+
### Gem name vs require path
|
|
157
157
|
|
|
158
|
-
The require
|
|
158
|
+
RubyGems normalizes names, so `DhanHQ` and `dhan_hq` refer to the same slot — the published name stays `DhanHQ` and will never change. The require path has used snake_case since v2.1.5:
|
|
159
159
|
|
|
160
160
|
```ruby
|
|
161
|
-
#
|
|
162
|
-
|
|
161
|
+
# Gemfile # Ruby file
|
|
162
|
+
gem 'DhanHQ' require 'dhan_hq'
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
### Optional features
|
|
166
|
+
|
|
167
|
+
The core SDK (`require 'dhan_hq'`) only loads the API client. Technical analysis and the options advisor are opt-in:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
require 'dhan_hq/analysis' # DhanHQ::Analysis::OptionsBuyingAdvisor, MultiTimeframeAnalyzer
|
|
171
|
+
require 'dhan_hq/ta' # TA::TechnicalAnalysis, TA::Fetcher, TA::Candles
|
|
172
|
+
```
|
|
166
173
|
|
|
167
174
|
---
|
|
168
175
|
|
|
@@ -485,6 +492,10 @@ bundle exec rubocop # lint
|
|
|
485
492
|
bin/console # interactive console
|
|
486
493
|
```
|
|
487
494
|
|
|
495
|
+
## Disclaimer
|
|
496
|
+
|
|
497
|
+
This gem is an independent, community-maintained project and is **not officially affiliated with, endorsed by, or supported by Dhan (Mirae Asset Capital Markets)**. Trading in financial instruments carries significant risk. Use this SDK at your own risk and always verify order placement in a sandbox environment before going live.
|
|
498
|
+
|
|
488
499
|
## License
|
|
489
500
|
|
|
490
501
|
[MIT](LICENSE.txt)
|
data/docs/TECHNICAL_ANALYSIS.md
CHANGED
|
@@ -65,7 +65,7 @@ indicators = TA::TechnicalAnalysis.new.compute_from_file(
|
|
|
65
65
|
## Analyze Multi-Timeframe Bias
|
|
66
66
|
|
|
67
67
|
```ruby
|
|
68
|
-
require "
|
|
68
|
+
require "dhan_hq/analysis"
|
|
69
69
|
|
|
70
70
|
analyzer = DhanHQ::Analysis::MultiTimeframeAnalyzer.new(data: indicators)
|
|
71
71
|
summary = analyzer.call
|
data/exe/DhanHQ
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# Dry-run order validator and human-readable summary for agent workflows.
|
|
6
|
+
class OrderPreview
|
|
7
|
+
attr_reader :params, :errors
|
|
8
|
+
|
|
9
|
+
def initialize(params)
|
|
10
|
+
@params = DhanHQ::Agent::ToolRegistry.symbolize(params)
|
|
11
|
+
@errors = []
|
|
12
|
+
validate
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def valid?
|
|
16
|
+
errors.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
valid: valid?,
|
|
22
|
+
errors: errors,
|
|
23
|
+
action: "place_order",
|
|
24
|
+
risk: "live_order_requires_confirmation",
|
|
25
|
+
requires: %w[orders:write DHANHQ_MCP_ENABLE_WRITES LIVE_TRADING],
|
|
26
|
+
summary: summary,
|
|
27
|
+
order: params
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate
|
|
34
|
+
result = DhanHQ::Contracts::PlaceOrderContract.new.call(params)
|
|
35
|
+
@errors << result.errors.to_h if result.failure?
|
|
36
|
+
@errors << "correlation_id is recommended for agent-originated orders" if params[:correlation_id].to_s.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def summary
|
|
40
|
+
side = params[:transaction_type]
|
|
41
|
+
qty = params[:quantity]
|
|
42
|
+
security = params[:security_id]
|
|
43
|
+
segment = params[:exchange_segment]
|
|
44
|
+
type = params[:order_type]
|
|
45
|
+
price = params[:price] ? " @ #{params[:price]}" : ""
|
|
46
|
+
"#{side} #{qty} of #{segment}:#{security} as #{type}#{price}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# Permission policy for MCP and agent-skill integrations.
|
|
6
|
+
class Policy
|
|
7
|
+
READ_SCOPES = %w[portfolio:read market:read orders:read].freeze
|
|
8
|
+
WRITE_SCOPES = %w[orders:write orders:cancel alerts:write risk:write].freeze
|
|
9
|
+
ALL_SCOPES = (READ_SCOPES + WRITE_SCOPES).freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :scopes
|
|
12
|
+
|
|
13
|
+
def initialize(scopes: [])
|
|
14
|
+
@scopes = Array(scopes).map(&:to_s).uniq.freeze
|
|
15
|
+
unknown = @scopes - ALL_SCOPES
|
|
16
|
+
raise ArgumentError, "Unknown agent scopes: #{unknown.join(", ")}" if unknown.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.read_only
|
|
20
|
+
new(scopes: READ_SCOPES)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.from_env
|
|
24
|
+
raw = ENV.fetch("DHANHQ_AGENT_SCOPES", READ_SCOPES.join(","))
|
|
25
|
+
new(scopes: raw.split(/[\s,]+/).reject(&:empty?))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def allow?(scope)
|
|
29
|
+
@scopes.include?(scope.to_s)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def require!(scope)
|
|
33
|
+
return true if allow?(scope)
|
|
34
|
+
|
|
35
|
+
raise DhanHQ::Error, "Agent scope required: #{scope}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def writes_enabled?
|
|
39
|
+
ENV["DHANHQ_MCP_ENABLE_WRITES"] == "true" && ENV["LIVE_TRADING"] == "true"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def require_write!(scope)
|
|
43
|
+
require!(scope)
|
|
44
|
+
return true if writes_enabled?
|
|
45
|
+
|
|
46
|
+
raise DhanHQ::LiveTradingDisabledError,
|
|
47
|
+
"Agent write tools require DHANHQ_MCP_ENABLE_WRITES=true and LIVE_TRADING=true"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# Machine-readable tool metadata shared by MCP and agent skills.
|
|
6
|
+
#
|
|
7
|
+
# Each tool has:
|
|
8
|
+
# - name, description, scope, risk, schema (input), handler
|
|
9
|
+
# - version: semantic version of the tool definition
|
|
10
|
+
# - output_schema: JSON Schema for the tool's return value
|
|
11
|
+
# - examples: array of example input/output pairs
|
|
12
|
+
# rubocop:disable Metrics/ModuleLength
|
|
13
|
+
module ToolRegistry
|
|
14
|
+
Tool = Struct.new(:name, :description, :scope, :risk, :schema, :handler,
|
|
15
|
+
:version, :output_schema, :examples) do
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
name: name,
|
|
19
|
+
description: description,
|
|
20
|
+
scope: scope,
|
|
21
|
+
risk: risk,
|
|
22
|
+
input_schema: schema,
|
|
23
|
+
output_schema: output_schema,
|
|
24
|
+
version: version,
|
|
25
|
+
examples: examples
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def tools
|
|
33
|
+
@tools ||= build_tools.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find(name)
|
|
37
|
+
tools.fetch(name.to_s) { raise ArgumentError, "Unknown DhanHQ agent tool: #{name}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list
|
|
41
|
+
tools.values.map(&:to_h)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def execute(name, arguments = {}, policy: Policy.from_env)
|
|
45
|
+
tool = find(name)
|
|
46
|
+
if tool.risk.to_s.end_with?("write") || tool.risk == "destructive_write"
|
|
47
|
+
policy.require_write!(tool.scope)
|
|
48
|
+
else
|
|
49
|
+
policy.require!(tool.scope)
|
|
50
|
+
end
|
|
51
|
+
tool.handler.call(symbolize(arguments))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns capability manifest for the agent runtime.
|
|
55
|
+
# Includes tool count, available scopes, risk levels, and version info.
|
|
56
|
+
def capabilities
|
|
57
|
+
{
|
|
58
|
+
version: DhanHQ::VERSION,
|
|
59
|
+
tool_count: tools.size,
|
|
60
|
+
tools: list,
|
|
61
|
+
scopes: Policy::ALL_SCOPES,
|
|
62
|
+
risk_levels: tools.values.map(&:risk).uniq.sort,
|
|
63
|
+
write_enabled: Policy.from_env.writes_enabled?
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_tools
|
|
68
|
+
[
|
|
69
|
+
tool("dhan_profile", "Fetch Dhan profile", "portfolio:read", "read_only",
|
|
70
|
+
object_schema, profile_handler,
|
|
71
|
+
version: "1.0.0",
|
|
72
|
+
output_schema: { type: "object", properties: { client_id: { type: "string" } } }),
|
|
73
|
+
tool("dhan_funds", "Fetch fund limits", "portfolio:read", "read_only",
|
|
74
|
+
object_schema, funds_handler,
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
output_schema: { type: "object", properties: { available_balance: { type: "number" } } }),
|
|
77
|
+
tool("dhan_holdings", "List holdings", "portfolio:read", "read_only",
|
|
78
|
+
object_schema, holdings_handler,
|
|
79
|
+
version: "1.0.0",
|
|
80
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
81
|
+
tool("dhan_positions", "List positions", "portfolio:read", "read_only",
|
|
82
|
+
object_schema, positions_handler,
|
|
83
|
+
version: "1.0.0",
|
|
84
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
85
|
+
tool("dhan_orders", "List orders", "orders:read", "read_only",
|
|
86
|
+
object_schema, orders_handler,
|
|
87
|
+
version: "1.0.0",
|
|
88
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
89
|
+
tool("dhan_trades", "List trades", "orders:read", "read_only",
|
|
90
|
+
object_schema, trades_handler,
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
93
|
+
tool("dhan_search_instruments", "Resolve symbols to security IDs", "market:read", "read_only",
|
|
94
|
+
search_schema, search_handler,
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
output_schema: { type: "array", items: { type: "object" } },
|
|
97
|
+
examples: [
|
|
98
|
+
{ input: { query: "RELIANCE" }, output: "[{security_id: '2885', symbol_name: 'RELIANCE'}]" }
|
|
99
|
+
]),
|
|
100
|
+
tool("dhan_ltp", "Fetch last traded prices", "market:read", "read_only",
|
|
101
|
+
feed_schema, ltp_handler,
|
|
102
|
+
version: "1.0.0",
|
|
103
|
+
output_schema: { type: "object", additionalProperties: { type: "number" } }),
|
|
104
|
+
tool("dhan_quote", "Fetch market quotes", "market:read", "read_only",
|
|
105
|
+
feed_schema, quote_handler,
|
|
106
|
+
version: "1.0.0",
|
|
107
|
+
output_schema: { type: "object", additionalProperties: { type: "object" } }),
|
|
108
|
+
tool("dhan_order_preview", "Validate and summarize an order without placing it", "orders:read",
|
|
109
|
+
"trade_adjacent_read", order_schema, preview_handler,
|
|
110
|
+
version: "1.0.0",
|
|
111
|
+
output_schema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
valid: { type: "boolean" },
|
|
115
|
+
errors: { type: "array" },
|
|
116
|
+
summary: { type: "string" }
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
tool("dhan_place_order", "Place an order after external confirmation", "orders:write", "live_write",
|
|
120
|
+
order_schema, place_order_handler,
|
|
121
|
+
version: "1.0.0",
|
|
122
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" } } }),
|
|
123
|
+
tool("dhan_cancel_order", "Cancel an order", "orders:cancel", "destructive_write",
|
|
124
|
+
cancel_schema, cancel_order_handler,
|
|
125
|
+
version: "1.0.0",
|
|
126
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" }, status: { type: "string" } } })
|
|
127
|
+
].to_h { |tool_item| [tool_item.name, tool_item] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# rubocop:disable Metrics/ParameterLists
|
|
131
|
+
def tool(name, description, scope, risk, schema, handler, version: "1.0.0", output_schema: nil, examples: nil)
|
|
132
|
+
Tool.new(
|
|
133
|
+
name: name, description: description, scope: scope, risk: risk,
|
|
134
|
+
schema: schema, handler: handler, version: version,
|
|
135
|
+
output_schema: output_schema, examples: examples
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
# rubocop:enable Metrics/ParameterLists
|
|
139
|
+
|
|
140
|
+
def object_schema
|
|
141
|
+
{ type: "object", properties: {}, additionalProperties: false }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def search_schema
|
|
145
|
+
{
|
|
146
|
+
type: "object",
|
|
147
|
+
required: ["query"],
|
|
148
|
+
properties: {
|
|
149
|
+
query: { type: "string" },
|
|
150
|
+
segments: { type: "array", items: { type: "string" } },
|
|
151
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
152
|
+
exact_match: { type: "boolean" }
|
|
153
|
+
},
|
|
154
|
+
additionalProperties: false
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def feed_schema
|
|
159
|
+
{
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["instruments"],
|
|
162
|
+
properties: {
|
|
163
|
+
instruments: {
|
|
164
|
+
type: "object",
|
|
165
|
+
additionalProperties: { type: "array", items: { type: %w[integer string] } }
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
additionalProperties: false
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def order_schema
|
|
173
|
+
{
|
|
174
|
+
type: "object",
|
|
175
|
+
required: %w[transaction_type exchange_segment product_type order_type validity security_id quantity],
|
|
176
|
+
properties: {
|
|
177
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
178
|
+
exchange_segment: { type: "string" },
|
|
179
|
+
product_type: { type: "string" },
|
|
180
|
+
order_type: { type: "string" },
|
|
181
|
+
validity: { type: "string" },
|
|
182
|
+
security_id: { type: "string" },
|
|
183
|
+
quantity: { type: "integer", minimum: 1 },
|
|
184
|
+
price: { type: "number" },
|
|
185
|
+
trigger_price: { type: "number" },
|
|
186
|
+
correlation_id: { type: "string" }
|
|
187
|
+
},
|
|
188
|
+
additionalProperties: true
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def cancel_schema
|
|
193
|
+
{
|
|
194
|
+
type: "object",
|
|
195
|
+
required: ["order_id"],
|
|
196
|
+
properties: { order_id: { type: "string" } },
|
|
197
|
+
additionalProperties: false
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def enum(values)
|
|
202
|
+
{ type: "string", enum: values }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def profile_handler = ->(_) { DhanHQ::Models::Profile.fetch }
|
|
206
|
+
|
|
207
|
+
def funds_handler = ->(_) { DhanHQ::Models::Funds.fetch }
|
|
208
|
+
|
|
209
|
+
def holdings_handler = ->(_) { DhanHQ::Models::Holding.all }
|
|
210
|
+
|
|
211
|
+
def positions_handler = ->(_) { DhanHQ::Models::Position.all }
|
|
212
|
+
|
|
213
|
+
def orders_handler = ->(_) { DhanHQ::Models::Order.all }
|
|
214
|
+
|
|
215
|
+
def trades_handler = ->(_) { DhanHQ::Models::Trade.today }
|
|
216
|
+
|
|
217
|
+
def search_handler
|
|
218
|
+
lambda do |arguments|
|
|
219
|
+
query = arguments.fetch(:query)
|
|
220
|
+
options = arguments.except(:query)
|
|
221
|
+
DhanHQ::Models::Instrument.search(query, **options)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def ltp_handler = ->(arguments) { DhanHQ::Models::MarketFeed.ltp(arguments[:instruments]) }
|
|
226
|
+
|
|
227
|
+
def quote_handler = ->(arguments) { DhanHQ::Models::MarketFeed.quote(arguments[:instruments]) }
|
|
228
|
+
|
|
229
|
+
def preview_handler = ->(arguments) { OrderPreview.new(arguments).to_h }
|
|
230
|
+
|
|
231
|
+
def place_order_handler = ->(arguments) { DhanHQ::Models::Order.place(arguments) }
|
|
232
|
+
|
|
233
|
+
def cancel_order_handler
|
|
234
|
+
lambda do |arguments|
|
|
235
|
+
order = DhanHQ::Models::Order.find(arguments[:order_id])
|
|
236
|
+
order&.cancel || false
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def symbolize(value)
|
|
241
|
+
case value
|
|
242
|
+
when Hash then value.each_with_object({}) { |(key, val), hash| hash[key.to_sym] = symbolize(val) }
|
|
243
|
+
when Array then value.map { |val| symbolize(val) }
|
|
244
|
+
else value
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
# rubocop:enable Metrics/ModuleLength
|
|
249
|
+
end
|
|
250
|
+
end
|
data/lib/DhanHQ/agent.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agent/policy"
|
|
4
|
+
require_relative "agent/tool_registry"
|
|
5
|
+
require_relative "agent/order_preview"
|
|
6
|
+
|
|
7
|
+
module DhanHQ
|
|
8
|
+
# Agent namespace provides primitives for AI-driven trading workflows.
|
|
9
|
+
# Includes policy enforcement, tool registry, and order preview capabilities.
|
|
10
|
+
module Agent
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# AI integration layer for building AI-powered trading assistants.
|
|
5
|
+
#
|
|
6
|
+
# Provides workflow orchestration, prompt helpers, and context
|
|
7
|
+
# serialization for AI agents.
|
|
8
|
+
#
|
|
9
|
+
# @example Build context for AI
|
|
10
|
+
# context = DhanHQ::AI::ContextBuilder.build do |ctx|
|
|
11
|
+
# ctx.add_portfolio
|
|
12
|
+
# ctx.add_positions
|
|
13
|
+
# ctx.add_recent_orders(limit: 10)
|
|
14
|
+
# ctx.add_market_snapshot(security_ids: ["2885"])
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
module AI
|
|
18
|
+
# Build context for AI agents from current account state.
|
|
19
|
+
#
|
|
20
|
+
# Provides methods to serialize portfolio, positions, orders,
|
|
21
|
+
# and market data into prompt-friendly formats.
|
|
22
|
+
class ContextBuilder
|
|
23
|
+
attr_reader :sections
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@sections = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build context using a block.
|
|
30
|
+
#
|
|
31
|
+
# @yield [builder] The context builder
|
|
32
|
+
# @return [DhanHQ::AI::ContextBuilder]
|
|
33
|
+
def self.build
|
|
34
|
+
builder = new
|
|
35
|
+
yield builder
|
|
36
|
+
builder
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add portfolio holdings to context.
|
|
40
|
+
def add_portfolio
|
|
41
|
+
holdings = DhanHQ::Models::Holding.all
|
|
42
|
+
@sections << {
|
|
43
|
+
type: :portfolio,
|
|
44
|
+
data: holdings.map(&:to_prompt),
|
|
45
|
+
summary: "Portfolio: #{holdings.size} holdings"
|
|
46
|
+
}
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add current positions to context.
|
|
51
|
+
def add_positions
|
|
52
|
+
positions = DhanHQ::Models::Position.all
|
|
53
|
+
active = positions.select(&:open?)
|
|
54
|
+
@sections << {
|
|
55
|
+
type: :positions,
|
|
56
|
+
data: active.map(&:to_prompt),
|
|
57
|
+
summary: "Positions: #{active.size} open"
|
|
58
|
+
}
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Add recent orders to context.
|
|
63
|
+
#
|
|
64
|
+
# @param limit [Integer] Number of recent orders (default: 10)
|
|
65
|
+
def add_recent_orders(limit: 10)
|
|
66
|
+
orders = DhanHQ::Models::Order.all
|
|
67
|
+
recent = orders.last(limit)
|
|
68
|
+
@sections << {
|
|
69
|
+
type: :orders,
|
|
70
|
+
data: recent.map(&:to_prompt),
|
|
71
|
+
summary: "Orders: #{orders.size} total, #{recent.size} recent"
|
|
72
|
+
}
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Add fund information to context.
|
|
77
|
+
def add_funds
|
|
78
|
+
funds = DhanHQ::Models::Funds.fetch
|
|
79
|
+
@sections << {
|
|
80
|
+
type: :funds,
|
|
81
|
+
data: [funds.to_prompt],
|
|
82
|
+
summary: "Funds: Available ₹#{funds.available_balance}"
|
|
83
|
+
}
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Add market snapshot to context.
|
|
88
|
+
#
|
|
89
|
+
# @param security_ids [Array<String>] Security IDs to fetch
|
|
90
|
+
def add_market_snapshot(security_ids:)
|
|
91
|
+
instruments = security_ids.each_with_object({}) do |sec_id, hash|
|
|
92
|
+
hash[DhanHQ::Constants::ExchangeSegment::NSE_EQ] ||= []
|
|
93
|
+
hash[DhanHQ::Constants::ExchangeSegment::NSE_EQ] << sec_id
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
response = DhanHQ::Models::MarketFeed.ltp(instruments)
|
|
97
|
+
snapshot = DhanHQ::MarketData::MarketSnapshot.from_response(response)
|
|
98
|
+
|
|
99
|
+
@sections << {
|
|
100
|
+
type: :market_data,
|
|
101
|
+
data: [snapshot.empty? ? "No data" : "Snapshot with #{snapshot.size} instruments"],
|
|
102
|
+
summary: "Market: #{snapshot.size} instruments"
|
|
103
|
+
}
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Add custom section to context.
|
|
108
|
+
#
|
|
109
|
+
# @param type [Symbol] Section type
|
|
110
|
+
# @param data [Array<String>] Prompt strings
|
|
111
|
+
# @param summary [String] Section summary
|
|
112
|
+
def add_section(type:, data:, summary:)
|
|
113
|
+
@sections << { type: type, data: data, summary: summary }
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Serialize context to a prompt string.
|
|
118
|
+
#
|
|
119
|
+
# @return [String] Formatted context string
|
|
120
|
+
def to_prompt
|
|
121
|
+
lines = ["=== DhanHQ Trading Context ==="]
|
|
122
|
+
lines << "Generated at: #{Time.now}"
|
|
123
|
+
lines << ""
|
|
124
|
+
|
|
125
|
+
@sections.each do |section|
|
|
126
|
+
lines << "--- #{section[:summary]} ---"
|
|
127
|
+
section[:data].each { |item| lines << " #{item}" }
|
|
128
|
+
lines << ""
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
lines.join("\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Serialize context to a hash.
|
|
135
|
+
#
|
|
136
|
+
# @return [Hash] Context as hash
|
|
137
|
+
def to_h
|
|
138
|
+
{
|
|
139
|
+
generated_at: Time.now,
|
|
140
|
+
sections: @sections
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|