DhanHQ 3.0.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/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/mcp.rb +3 -0
- data/lib/dhan_hq.rb +27 -2
- 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 +59 -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:
|
|
@@ -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
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module AI
|
|
5
|
+
# Helper methods for generating prompts for AI trading assistants.
|
|
6
|
+
#
|
|
7
|
+
# Provides methods to create system prompts, user prompts,
|
|
8
|
+
# and context summaries for AI models.
|
|
9
|
+
module PromptHelpers
|
|
10
|
+
# Generate a system prompt for an AI trading assistant.
|
|
11
|
+
#
|
|
12
|
+
# @param capabilities [Array<String>] List of capabilities
|
|
13
|
+
# @return [String] System prompt
|
|
14
|
+
def self.system_prompt(capabilities: [])
|
|
15
|
+
<<~PROMPT.strip
|
|
16
|
+
You are an AI trading assistant for Indian stock markets (NSE, BSE, MCX).
|
|
17
|
+
|
|
18
|
+
Your capabilities:
|
|
19
|
+
- Fetch market data (LTP, OHLC, quotes)
|
|
20
|
+
- Place, modify, and cancel orders
|
|
21
|
+
- View portfolio holdings, positions, and orders
|
|
22
|
+
- Calculate option Greeks and implied volatility
|
|
23
|
+
- Analyze option chains and Max Pain
|
|
24
|
+
- Apply risk management rules
|
|
25
|
+
|
|
26
|
+
#{"Additional capabilities:\n#{capabilities.map { |c| "- #{c}" }.join("\n")}" unless capabilities.empty?}
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- Always confirm before placing live orders
|
|
30
|
+
- Use correlation_id for all agent-originated orders
|
|
31
|
+
- Never expose access tokens or secrets
|
|
32
|
+
- Prefer read-only operations unless explicitly asked to trade
|
|
33
|
+
- Validate instruments before trading using search
|
|
34
|
+
PROMPT
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Generate a portfolio summary prompt.
|
|
38
|
+
#
|
|
39
|
+
# @param holdings [Array<DhanHQ::Models::Holding>] Holdings data
|
|
40
|
+
# @param positions [Array<DhanHQ::Models::Position>] Positions data
|
|
41
|
+
# @param funds [DhanHQ::Models::Funds] Funds data
|
|
42
|
+
# @return [String] Portfolio summary
|
|
43
|
+
def self.portfolio_summary(holdings:, positions:, funds:)
|
|
44
|
+
lines = ["=== Portfolio Summary ==="]
|
|
45
|
+
lines << "Funds: #{funds.to_prompt}" if funds
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << "Holdings (#{holdings.size}):"
|
|
48
|
+
holdings.each { |h| lines << " #{h.to_prompt}" }
|
|
49
|
+
lines << ""
|
|
50
|
+
lines << "Open Positions (#{positions.count(&:open?)}):"
|
|
51
|
+
positions.select(&:open?).each { |p| lines << " #{p.to_prompt}" }
|
|
52
|
+
lines.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate a market analysis prompt.
|
|
56
|
+
#
|
|
57
|
+
# @param snapshot [DhanHQ::MarketData::MarketSnapshot] Market snapshot
|
|
58
|
+
# @param series [DhanHQ::MarketData::OHLCSeries] OHLC series
|
|
59
|
+
# @return [String] Market analysis prompt
|
|
60
|
+
def self.market_analysis(snapshot:, series: nil)
|
|
61
|
+
lines = ["=== Market Analysis ==="]
|
|
62
|
+
lines << "Snapshot: #{snapshot.size} instruments"
|
|
63
|
+
lines << "Series: #{series&.size || 0} candles"
|
|
64
|
+
|
|
65
|
+
if series&.any?
|
|
66
|
+
lines << "Latest close: ₹#{series.last.close}"
|
|
67
|
+
lines << "Average close: ₹#{series.average_close&.round(2)}"
|
|
68
|
+
lines << "Price range: ₹#{series.price_range&.round(2)}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Generate an order confirmation prompt.
|
|
75
|
+
#
|
|
76
|
+
# @param order_params [Hash] Order parameters
|
|
77
|
+
# @return [String] Order confirmation prompt
|
|
78
|
+
def self.order_confirmation(order_params)
|
|
79
|
+
<<~PROMPT.strip
|
|
80
|
+
=== Order Confirmation Required ===
|
|
81
|
+
#{order_params[:transaction_type]} #{order_params[:quantity]}x #{order_params[:security_id]}
|
|
82
|
+
Exchange: #{order_params[:exchange_segment]}
|
|
83
|
+
Product: #{order_params[:product_type]}
|
|
84
|
+
Type: #{order_params[:order_type]}
|
|
85
|
+
#{order_params[:price] ? "Price: ₹#{order_params[:price]}" : "Market Price"}
|
|
86
|
+
#{"Trigger: ₹#{order_params[:trigger_price]}" if order_params[:trigger_price]}
|
|
87
|
+
|
|
88
|
+
Please confirm this order.
|
|
89
|
+
PROMPT
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate a risk report prompt.
|
|
93
|
+
#
|
|
94
|
+
# @param positions [Array<DhanHQ::Models::Position>] Current positions
|
|
95
|
+
# @param risk_params [Hash] Risk parameters
|
|
96
|
+
# @return [String] Risk report
|
|
97
|
+
def self.risk_report(positions:, risk_params: {})
|
|
98
|
+
lines = ["=== Risk Report ==="]
|
|
99
|
+
total_unrealized = positions.sum { |p| p.unrealized_profit.to_f }
|
|
100
|
+
total_realized = positions.sum { |p| p.realized_profit.to_f }
|
|
101
|
+
|
|
102
|
+
lines << "Total Unrealized P&L: ₹#{total_unrealized.round(2)}"
|
|
103
|
+
lines << "Total Realized P&L: ₹#{total_realized.round(2)}"
|
|
104
|
+
lines << "Open Positions: #{positions.count(&:open?)}"
|
|
105
|
+
|
|
106
|
+
lines << "Max Drawdown: #{risk_params[:max_drawdown]}%" if risk_params[:max_drawdown]
|
|
107
|
+
|
|
108
|
+
lines << "Daily Loss Limit: ₹#{risk_params[:daily_loss_limit]}" if risk_params[:daily_loss_limit]
|
|
109
|
+
|
|
110
|
+
lines.join("\n")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/DhanHQ/ai.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ai/context_builder"
|
|
4
|
+
require_relative "ai/prompt_helpers"
|
|
5
|
+
|
|
6
|
+
module DhanHQ
|
|
7
|
+
# AI integration layer for building AI-powered trading assistants.
|
|
8
|
+
#
|
|
9
|
+
# Provides workflow orchestration, prompt helpers, and context
|
|
10
|
+
# serialization for AI agents.
|
|
11
|
+
#
|
|
12
|
+
# @example Build context for AI
|
|
13
|
+
# context = DhanHQ::AI::ContextBuilder.build do |ctx|
|
|
14
|
+
# ctx.add_portfolio
|
|
15
|
+
# ctx.add_positions
|
|
16
|
+
# ctx.add_recent_orders(limit: 10)
|
|
17
|
+
# end
|
|
18
|
+
# puts context.to_prompt
|
|
19
|
+
#
|
|
20
|
+
# @example Generate system prompt
|
|
21
|
+
# prompt = DhanHQ::AI::PromptHelpers.system_prompt(
|
|
22
|
+
# capabilities: ["Option chain analysis", "Greeks calculation"]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
module AI
|
|
26
|
+
end
|
|
27
|
+
end
|