DhanHQ 3.0.0 → 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/.rubocop_todo.yml +7 -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/order_preview.rb +50 -0
- data/lib/DhanHQ/agent/policy.rb +51 -0
- data/lib/DhanHQ/agent/tool_registry.rb +299 -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/concerns/order_audit.rb +43 -1
- data/lib/DhanHQ/constants.rb +5 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- 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 +235 -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 +80 -14
- 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/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 +62 -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 +62 -0
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
- data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
- data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
- data/lib/DhanHQ/risk/checks/max_loss.rb +24 -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/position_limits.rb +24 -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 +72 -0
- data/lib/DhanHQ/risk.rb +250 -0
- data/lib/DhanHQ/skills/base.rb +183 -0
- 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 +84 -0
- data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +89 -0
- 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 +42 -0
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +50 -0
- data/lib/DhanHQ/skills/builtin/straddle.rb +88 -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 +74 -2
- data/lib/ta/technical_analysis.rb +3 -1
- data/skills/dhanhq-ruby/SKILL.md +207 -0
- 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 +201 -0
- 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 +95 -20
|
@@ -0,0 +1,299 @@
|
|
|
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
|
+
(primitive_tools + skill_tools).to_h { |tool_item| [tool_item.name, tool_item] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def primitive_tools
|
|
72
|
+
[
|
|
73
|
+
tool("dhan_profile", "Fetch Dhan profile", "portfolio:read", "read_only",
|
|
74
|
+
object_schema, profile_handler,
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
output_schema: { type: "object", properties: { client_id: { type: "string" } } }),
|
|
77
|
+
tool("dhan_funds", "Fetch fund limits", "portfolio:read", "read_only",
|
|
78
|
+
object_schema, funds_handler,
|
|
79
|
+
version: "1.0.0",
|
|
80
|
+
output_schema: { type: "object", properties: { available_balance: { type: "number" } } }),
|
|
81
|
+
tool("dhan_holdings", "List holdings", "portfolio:read", "read_only",
|
|
82
|
+
object_schema, holdings_handler,
|
|
83
|
+
version: "1.0.0",
|
|
84
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
85
|
+
tool("dhan_positions", "List positions", "portfolio:read", "read_only",
|
|
86
|
+
object_schema, positions_handler,
|
|
87
|
+
version: "1.0.0",
|
|
88
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
89
|
+
tool("dhan_orders", "List orders", "orders:read", "read_only",
|
|
90
|
+
object_schema, orders_handler,
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
93
|
+
tool("dhan_trades", "List trades", "orders:read", "read_only",
|
|
94
|
+
object_schema, trades_handler,
|
|
95
|
+
version: "1.0.0",
|
|
96
|
+
output_schema: { type: "array", items: { type: "object" } }),
|
|
97
|
+
tool("dhan_search_instruments", "Resolve symbols to security IDs", "market:read", "read_only",
|
|
98
|
+
search_schema, search_handler,
|
|
99
|
+
version: "1.0.0",
|
|
100
|
+
output_schema: { type: "array", items: { type: "object" } },
|
|
101
|
+
examples: [
|
|
102
|
+
{ input: { query: "RELIANCE" }, output: "[{security_id: '2885', symbol_name: 'RELIANCE'}]" }
|
|
103
|
+
]),
|
|
104
|
+
tool("dhan_ltp", "Fetch last traded prices", "market:read", "read_only",
|
|
105
|
+
feed_schema, ltp_handler,
|
|
106
|
+
version: "1.0.0",
|
|
107
|
+
output_schema: { type: "object", additionalProperties: { type: "number" } }),
|
|
108
|
+
tool("dhan_quote", "Fetch market quotes", "market:read", "read_only",
|
|
109
|
+
feed_schema, quote_handler,
|
|
110
|
+
version: "1.0.0",
|
|
111
|
+
output_schema: { type: "object", additionalProperties: { type: "object" } }),
|
|
112
|
+
tool("dhan_order_preview", "Validate and summarize an order without placing it", "orders:read",
|
|
113
|
+
"trade_adjacent_read", order_schema, preview_handler,
|
|
114
|
+
version: "1.0.0",
|
|
115
|
+
output_schema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
valid: { type: "boolean" },
|
|
119
|
+
errors: { type: "array" },
|
|
120
|
+
summary: { type: "string" }
|
|
121
|
+
}
|
|
122
|
+
}),
|
|
123
|
+
tool("dhan_place_order", "Place an order after external confirmation", "orders:write", "live_write",
|
|
124
|
+
order_schema, place_order_handler,
|
|
125
|
+
version: "1.0.0",
|
|
126
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" } } }),
|
|
127
|
+
tool("dhan_cancel_order", "Cancel an order", "orders:cancel", "destructive_write",
|
|
128
|
+
cancel_schema, cancel_order_handler,
|
|
129
|
+
version: "1.0.0",
|
|
130
|
+
output_schema: { type: "object", properties: { order_id: { type: "string" }, status: { type: "string" } } })
|
|
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")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# rubocop:disable Metrics/ParameterLists
|
|
159
|
+
def tool(name, description, scope, risk, schema, handler, version: "1.0.0", output_schema: nil, examples: nil)
|
|
160
|
+
Tool.new(
|
|
161
|
+
name: name, description: description, scope: scope, risk: risk,
|
|
162
|
+
schema: schema, handler: handler, version: version,
|
|
163
|
+
output_schema: output_schema, examples: examples
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
# rubocop:enable Metrics/ParameterLists
|
|
167
|
+
|
|
168
|
+
def object_schema
|
|
169
|
+
{ type: "object", properties: {}, additionalProperties: false }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def search_schema
|
|
173
|
+
{
|
|
174
|
+
type: "object",
|
|
175
|
+
required: ["query"],
|
|
176
|
+
properties: {
|
|
177
|
+
query: { type: "string" },
|
|
178
|
+
segments: { type: "array", items: { type: "string" } },
|
|
179
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
180
|
+
exact_match: { type: "boolean" }
|
|
181
|
+
},
|
|
182
|
+
additionalProperties: false
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def feed_schema
|
|
187
|
+
{
|
|
188
|
+
type: "object",
|
|
189
|
+
required: ["instruments"],
|
|
190
|
+
properties: {
|
|
191
|
+
instruments: {
|
|
192
|
+
type: "object",
|
|
193
|
+
additionalProperties: { type: "array", items: { type: %w[integer string] } }
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
additionalProperties: false
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def order_schema
|
|
201
|
+
{
|
|
202
|
+
type: "object",
|
|
203
|
+
required: %w[transaction_type exchange_segment product_type order_type validity security_id quantity],
|
|
204
|
+
properties: {
|
|
205
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
206
|
+
exchange_segment: { type: "string" },
|
|
207
|
+
product_type: { type: "string" },
|
|
208
|
+
order_type: { type: "string" },
|
|
209
|
+
validity: { type: "string" },
|
|
210
|
+
security_id: { type: "string" },
|
|
211
|
+
quantity: { type: "integer", minimum: 1 },
|
|
212
|
+
price: { type: "number" },
|
|
213
|
+
trigger_price: { type: "number" },
|
|
214
|
+
correlation_id: { type: "string" }
|
|
215
|
+
},
|
|
216
|
+
additionalProperties: true
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def cancel_schema
|
|
221
|
+
{
|
|
222
|
+
type: "object",
|
|
223
|
+
required: ["order_id"],
|
|
224
|
+
properties: { order_id: { type: "string" } },
|
|
225
|
+
additionalProperties: false
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def enum(values)
|
|
230
|
+
{ type: "string", enum: values }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def profile_handler = ->(_) { DhanHQ::Models::Profile.fetch }
|
|
234
|
+
|
|
235
|
+
def funds_handler = ->(_) { DhanHQ::Models::Funds.fetch }
|
|
236
|
+
|
|
237
|
+
def holdings_handler = ->(_) { DhanHQ::Models::Holding.all }
|
|
238
|
+
|
|
239
|
+
def positions_handler = ->(_) { DhanHQ::Models::Position.all }
|
|
240
|
+
|
|
241
|
+
def orders_handler = ->(_) { DhanHQ::Models::Order.all }
|
|
242
|
+
|
|
243
|
+
def trades_handler = ->(_) { DhanHQ::Models::Trade.today }
|
|
244
|
+
|
|
245
|
+
def search_handler
|
|
246
|
+
lambda do |arguments|
|
|
247
|
+
query = arguments.fetch(:query)
|
|
248
|
+
options = arguments.except(:query)
|
|
249
|
+
DhanHQ::Models::Instrument.search(query, **options)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def ltp_handler = ->(arguments) { DhanHQ::Models::MarketFeed.ltp(arguments[:instruments]) }
|
|
254
|
+
|
|
255
|
+
def quote_handler = ->(arguments) { DhanHQ::Models::MarketFeed.quote(arguments[:instruments]) }
|
|
256
|
+
|
|
257
|
+
def preview_handler = ->(arguments) { OrderPreview.new(arguments).to_h }
|
|
258
|
+
|
|
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
|
|
273
|
+
|
|
274
|
+
def cancel_order_handler
|
|
275
|
+
lambda do |arguments|
|
|
276
|
+
order = DhanHQ::Models::Order.find(arguments[:order_id])
|
|
277
|
+
order&.cancel || false
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def symbolize(value)
|
|
282
|
+
case value
|
|
283
|
+
when Hash then value.each_with_object({}) { |(key, val), hash| hash[key.to_sym] = symbolize(val) }
|
|
284
|
+
when Array then value.map { |val| symbolize(val) }
|
|
285
|
+
else value
|
|
286
|
+
end
|
|
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
|
|
296
|
+
end
|
|
297
|
+
# rubocop:enable Metrics/ModuleLength
|
|
298
|
+
end
|
|
299
|
+
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
|
data/lib/DhanHQ/auth.rb
CHANGED
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -212,9 +212,7 @@ module DhanHQ
|
|
|
212
212
|
write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
|
|
213
213
|
|
|
214
214
|
Faraday.new(url: url) do |conn|
|
|
215
|
-
conn.request :
|
|
216
|
-
conn.response :json, content_type: /\bjson$/
|
|
217
|
-
conn.response :logger if ENV["DHAN_DEBUG"] == "true"
|
|
215
|
+
conn.request :url_encoded
|
|
218
216
|
conn.options.timeout = read_timeout
|
|
219
217
|
conn.options.open_timeout = connect_timeout
|
|
220
218
|
conn.options.write_timeout = write_timeout
|
|
@@ -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 = {})
|