DhanHQ 3.1.0 → 3.2.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/ARCHITECTURE.md +47 -1
- data/CHANGELOG.md +73 -0
- data/README.md +241 -3
- data/docs/AUTHENTICATION.md +3 -5
- data/docs/CONFIGURATION.md +3 -2
- data/docs/CONSTANTS_REFERENCE.md +5 -4
- 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/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 -261
- data/lib/DhanHQ/agent/tool_schemas.rb +160 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +105 -30
- data/lib/DhanHQ/concerns/order_audit.rb +35 -4
- data/lib/DhanHQ/configuration.rb +70 -1
- data/lib/DhanHQ/constants.rb +101 -0
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +1 -9
- 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/multi_order_contract.rb +74 -0
- data/lib/DhanHQ/contracts/trade_history_contract.rb +1 -8
- 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/models/alert_order.rb +6 -3
- 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/margin.rb +5 -1
- data/lib/DhanHQ/models/multi_order.rb +130 -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/multi_orders.rb +58 -0
- data/lib/DhanHQ/risk/pipeline.rb +0 -2
- 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 +4 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
- data/skills/dhanhq-ruby/SKILL.md +2 -1
- data/skills/dhanhq-ruby/examples/order_management.rb +7 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +8 -8
- data/skills/dhanhq-ruby/references/instruments.md +7 -1
- data/skills/dhanhq-ruby/references/orders.md +2 -0
- metadata +30 -10
- data/.rspec +0 -3
- data/.rubocop.yml +0 -50
- data/.rubocop_todo.yml +0 -217
- data/Rakefile +0 -14
- data/TAGS +0 -10
- data/core +0 -0
- data/diagram.html +0 -184
- data/watchlist.csv +0 -3
|
@@ -2,35 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Agent
|
|
5
|
-
#
|
|
5
|
+
# Registry and dispatcher for the tools exposed to MCP clients and agent skills.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# - examples: array of example input/output pairs
|
|
12
|
-
# rubocop:disable Metrics/ModuleLength
|
|
7
|
+
# Holds the catalogue (which tools exist, at what scope and risk) and enforces
|
|
8
|
+
# {DhanHQ::Agent::Policy} on every call. The tools themselves are described by
|
|
9
|
+
# {DhanHQ::Agent::ToolSchemas} and carried out by {DhanHQ::Agent::ToolHandlers};
|
|
10
|
+
# each tool is a {DhanHQ::Agent::Tool}.
|
|
13
11
|
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
12
|
module_function
|
|
31
13
|
|
|
32
14
|
def tools
|
|
33
|
-
@tools ||=
|
|
15
|
+
@tools ||= ToolCatalogue.all.freeze
|
|
34
16
|
end
|
|
35
17
|
|
|
36
18
|
def find(name)
|
|
@@ -43,12 +25,17 @@ module DhanHQ
|
|
|
43
25
|
|
|
44
26
|
def execute(name, arguments = {}, policy: Policy.from_env)
|
|
45
27
|
tool = find(name)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
28
|
+
tool.write? ? policy.require_write!(tool.scope) : policy.require!(tool.scope)
|
|
29
|
+
tool.handler.call(KeyCoercion.symbolize(arguments))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Coerces JSON-delivered argument keys to symbols.
|
|
33
|
+
# Part of this module's surface because {DhanHQ::Agent::OrderPreview} calls it.
|
|
34
|
+
#
|
|
35
|
+
# @param value [Hash, Array, Object]
|
|
36
|
+
# @return [Hash, Array, Object]
|
|
37
|
+
def symbolize(value)
|
|
38
|
+
KeyCoercion.symbolize(value)
|
|
52
39
|
end
|
|
53
40
|
|
|
54
41
|
# Returns capability manifest for the agent runtime.
|
|
@@ -63,237 +50,6 @@ module DhanHQ
|
|
|
63
50
|
write_enabled: Policy.from_env.writes_enabled?
|
|
64
51
|
}
|
|
65
52
|
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
53
|
end
|
|
297
|
-
# rubocop:enable Metrics/ModuleLength
|
|
298
54
|
end
|
|
299
55
|
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Agent
|
|
5
|
+
# JSON Schema fragments describing the input each agent tool accepts.
|
|
6
|
+
#
|
|
7
|
+
# Pure data: no API calls, no policy, no state. Kept apart from
|
|
8
|
+
# {DhanHQ::Agent::ToolRegistry} so that adding an endpoint means editing a schema
|
|
9
|
+
# here and a handler in {DhanHQ::Agent::ToolHandlers}, rather than growing one
|
|
10
|
+
# module that does registration, schema description and dispatch at once.
|
|
11
|
+
module ToolSchemas
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def object_schema
|
|
15
|
+
{ type: "object", properties: {}, additionalProperties: false }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def search_schema
|
|
19
|
+
{
|
|
20
|
+
type: "object",
|
|
21
|
+
required: ["query"],
|
|
22
|
+
properties: {
|
|
23
|
+
query: { type: "string" },
|
|
24
|
+
segments: { type: "array", items: { type: "string" } },
|
|
25
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
26
|
+
exact_match: { type: "boolean" }
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def feed_schema
|
|
33
|
+
{
|
|
34
|
+
type: "object",
|
|
35
|
+
required: ["instruments"],
|
|
36
|
+
properties: {
|
|
37
|
+
instruments: {
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: { type: "array", items: { type: %w[integer string] } }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def order_schema
|
|
47
|
+
{
|
|
48
|
+
type: "object",
|
|
49
|
+
required: %w[transaction_type exchange_segment product_type order_type validity security_id quantity],
|
|
50
|
+
properties: {
|
|
51
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
52
|
+
exchange_segment: { type: "string" },
|
|
53
|
+
product_type: { type: "string" },
|
|
54
|
+
order_type: { type: "string" },
|
|
55
|
+
validity: { type: "string" },
|
|
56
|
+
security_id: { type: "string" },
|
|
57
|
+
quantity: { type: "integer", minimum: 1 },
|
|
58
|
+
price: { type: "number" },
|
|
59
|
+
trigger_price: { type: "number" },
|
|
60
|
+
correlation_id: { type: "string" }
|
|
61
|
+
},
|
|
62
|
+
additionalProperties: true
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cancel_schema
|
|
67
|
+
{
|
|
68
|
+
type: "object",
|
|
69
|
+
required: ["order_id"],
|
|
70
|
+
properties: { order_id: { type: "string" } },
|
|
71
|
+
additionalProperties: false
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Global Stocks orders carry no exchange segment, product type or validity, and
|
|
76
|
+
# quantity is fractional. AMOUNT orders replace quantity with a dollar amount.
|
|
77
|
+
def global_order_schema
|
|
78
|
+
{
|
|
79
|
+
type: "object",
|
|
80
|
+
required: %w[transaction_type order_type security_id],
|
|
81
|
+
properties: {
|
|
82
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
83
|
+
order_type: enum(DhanHQ::Constants::GlobalStocks::OrderType::ALL),
|
|
84
|
+
security_id: { type: "string" },
|
|
85
|
+
quantity: { type: "number", exclusiveMinimum: 0 },
|
|
86
|
+
price: { type: "number", minimum: 0 },
|
|
87
|
+
trigger_price: { type: "number" },
|
|
88
|
+
stop_loss_price: { type: "number" },
|
|
89
|
+
target_price: { type: "number" },
|
|
90
|
+
amount: { type: "number", description: "Dollar value for AMOUNT orders" },
|
|
91
|
+
correlation_id: { type: "string" }
|
|
92
|
+
},
|
|
93
|
+
additionalProperties: true
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def global_estimate_schema
|
|
98
|
+
{
|
|
99
|
+
type: "object",
|
|
100
|
+
required: %w[security_id transaction_type price quantity],
|
|
101
|
+
properties: {
|
|
102
|
+
security_id: { type: "string" },
|
|
103
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
104
|
+
price: { type: "number", minimum: 0 },
|
|
105
|
+
quantity: { type: "number", exclusiveMinimum: 0 }
|
|
106
|
+
},
|
|
107
|
+
additionalProperties: false
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def multi_order_schema
|
|
112
|
+
{
|
|
113
|
+
type: "object",
|
|
114
|
+
required: ["orders"],
|
|
115
|
+
properties: {
|
|
116
|
+
orders: {
|
|
117
|
+
type: "array",
|
|
118
|
+
minItems: 1,
|
|
119
|
+
maxItems: DhanHQ::Contracts::MultiOrderContract::MAX_ORDERS,
|
|
120
|
+
items: {
|
|
121
|
+
type: "object",
|
|
122
|
+
required: %w[sequence transaction_type exchange_segment],
|
|
123
|
+
properties: {
|
|
124
|
+
sequence: { type: "string" },
|
|
125
|
+
transaction_type: enum(%w[BUY SELL]),
|
|
126
|
+
exchange_segment: { type: "string" },
|
|
127
|
+
product_type: { type: "string" },
|
|
128
|
+
order_type: { type: "string" },
|
|
129
|
+
validity: { type: "string" },
|
|
130
|
+
security_id: { type: "string" },
|
|
131
|
+
quantity: { type: "integer", minimum: 1 },
|
|
132
|
+
price: { type: "number" },
|
|
133
|
+
trigger_price: { type: "number" }
|
|
134
|
+
},
|
|
135
|
+
additionalProperties: true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
additionalProperties: false
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def enum(values)
|
|
144
|
+
{ type: "string", enum: values }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def skill_input_schema(params)
|
|
148
|
+
properties = params.transform_values do |config|
|
|
149
|
+
{ type: skill_param_type(config[:type]) }.tap { |h| h[:description] = config[:description] if config[:description] }
|
|
150
|
+
end
|
|
151
|
+
required = params.select { |_, config| config[:required] }.keys.map(&:to_s)
|
|
152
|
+
{ type: "object", properties: properties, required: required, additionalProperties: false }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def skill_param_type(type)
|
|
156
|
+
{ string: "string", integer: "integer", number: "number", boolean: "boolean" }.fetch(type.to_sym, "string")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -41,14 +41,20 @@ module DhanHQ
|
|
|
41
41
|
# @param funds [DhanHQ::Models::Funds] Funds data
|
|
42
42
|
# @return [String] Portfolio summary
|
|
43
43
|
def self.portfolio_summary(holdings:, positions:, funds:)
|
|
44
|
+
# Array() so a missing collection renders as an empty section instead of
|
|
45
|
+
# raising. `funds` was already guarded; these two were not, and a prompt
|
|
46
|
+
# helper failing hard on an empty portfolio is the wrong trade.
|
|
47
|
+
all_holdings = Array(holdings)
|
|
48
|
+
open_positions = Array(positions).select(&:open?)
|
|
49
|
+
|
|
44
50
|
lines = ["=== Portfolio Summary ==="]
|
|
45
51
|
lines << "Funds: #{funds.to_prompt}" if funds
|
|
46
52
|
lines << ""
|
|
47
|
-
lines << "Holdings (#{
|
|
48
|
-
|
|
53
|
+
lines << "Holdings (#{all_holdings.size}):"
|
|
54
|
+
all_holdings.each { |holding| lines << " #{holding.to_prompt}" }
|
|
49
55
|
lines << ""
|
|
50
|
-
lines << "Open Positions (#{
|
|
51
|
-
|
|
56
|
+
lines << "Open Positions (#{open_positions.size}):"
|
|
57
|
+
open_positions.each { |position| lines << " #{position.to_prompt}" }
|
|
52
58
|
lines.join("\n")
|
|
53
59
|
end
|
|
54
60
|
|
|
@@ -95,13 +101,17 @@ module DhanHQ
|
|
|
95
101
|
# @param risk_params [Hash] Risk parameters
|
|
96
102
|
# @return [String] Risk report
|
|
97
103
|
def self.risk_report(positions:, risk_params: {})
|
|
104
|
+
# P&L is summed across every position; only the count is restricted to open
|
|
105
|
+
# ones, since a closed position still contributed realised P&L today.
|
|
106
|
+
all_positions = Array(positions)
|
|
107
|
+
|
|
98
108
|
lines = ["=== Risk Report ==="]
|
|
99
|
-
total_unrealized =
|
|
100
|
-
total_realized =
|
|
109
|
+
total_unrealized = all_positions.sum { |position| position.unrealized_profit.to_f }
|
|
110
|
+
total_realized = all_positions.sum { |position| position.realized_profit.to_f }
|
|
101
111
|
|
|
102
112
|
lines << "Total Unrealized P&L: ₹#{total_unrealized.round(2)}"
|
|
103
113
|
lines << "Total Realized P&L: ₹#{total_realized.round(2)}"
|
|
104
|
-
lines << "Open Positions: #{
|
|
114
|
+
lines << "Open Positions: #{all_positions.count(&:open?)}"
|
|
105
115
|
|
|
106
116
|
lines << "Max Drawdown: #{risk_params[:max_drawdown]}%" if risk_params[:max_drawdown]
|
|
107
117
|
|