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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +7 -0
  4. data/CHANGELOG.md +51 -0
  5. data/README.md +152 -4
  6. data/docs/CONSTANTS_REFERENCE.md +3 -2
  7. data/exe/dhanhq-mcp +7 -0
  8. data/lib/DhanHQ/agent/order_preview.rb +50 -0
  9. data/lib/DhanHQ/agent/policy.rb +51 -0
  10. data/lib/DhanHQ/agent/tool_registry.rb +299 -0
  11. data/lib/DhanHQ/agent.rb +12 -0
  12. data/lib/DhanHQ/ai/context_builder.rb +145 -0
  13. data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
  14. data/lib/DhanHQ/ai.rb +27 -0
  15. data/lib/DhanHQ/auth.rb +0 -1
  16. data/lib/DhanHQ/client.rb +1 -3
  17. data/lib/DhanHQ/concerns/order_audit.rb +43 -1
  18. data/lib/DhanHQ/constants.rb +5 -2
  19. data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
  20. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
  21. data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
  22. data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
  23. data/lib/DhanHQ/core/auth_api.rb +0 -1
  24. data/lib/DhanHQ/errors.rb +4 -0
  25. data/lib/DhanHQ/events/base.rb +203 -0
  26. data/lib/DhanHQ/events/bus.rb +158 -0
  27. data/lib/DhanHQ/events.rb +40 -0
  28. data/lib/DhanHQ/indicators.rb +283 -0
  29. data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
  30. data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
  31. data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
  32. data/lib/DhanHQ/market_data.rb +25 -0
  33. data/lib/DhanHQ/mcp/server.rb +235 -0
  34. data/lib/DhanHQ/mcp.rb +10 -0
  35. data/lib/DhanHQ/models/funds.rb +12 -0
  36. data/lib/DhanHQ/models/holding.rb +42 -0
  37. data/lib/DhanHQ/models/iceberg_order.rb +139 -0
  38. data/lib/DhanHQ/models/instrument.rb +80 -14
  39. data/lib/DhanHQ/models/order.rb +95 -0
  40. data/lib/DhanHQ/models/position.rb +66 -0
  41. data/lib/DhanHQ/models/search_result.rb +12 -0
  42. data/lib/DhanHQ/models/trade.rb +13 -0
  43. data/lib/DhanHQ/models/twap_order.rb +136 -0
  44. data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
  45. data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
  46. data/lib/DhanHQ/option_analytics.rb +36 -0
  47. data/lib/DhanHQ/rate_limiter.rb +5 -3
  48. data/lib/DhanHQ/resources/alert_orders.rb +1 -0
  49. data/lib/DhanHQ/resources/forever_orders.rb +1 -0
  50. data/lib/DhanHQ/resources/iceberg_orders.rb +62 -0
  51. data/lib/DhanHQ/resources/orders.rb +2 -0
  52. data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
  53. data/lib/DhanHQ/resources/super_orders.rb +1 -0
  54. data/lib/DhanHQ/resources/twap_orders.rb +62 -0
  55. data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
  56. data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
  57. data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
  58. data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
  59. data/lib/DhanHQ/risk/checks/options.rb +46 -0
  60. data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
  61. data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
  62. data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
  63. data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
  64. data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
  65. data/lib/DhanHQ/risk/pipeline.rb +72 -0
  66. data/lib/DhanHQ/risk.rb +250 -0
  67. data/lib/DhanHQ/skills/base.rb +183 -0
  68. data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
  69. data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
  70. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +84 -0
  71. data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
  72. data/lib/DhanHQ/skills/builtin/iron_condor.rb +89 -0
  73. data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
  74. data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
  75. data/lib/DhanHQ/skills/builtin/square_off_all.rb +42 -0
  76. data/lib/DhanHQ/skills/builtin/square_off_position.rb +50 -0
  77. data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
  78. data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
  79. data/lib/DhanHQ/skills/registry.rb +101 -0
  80. data/lib/DhanHQ/skills/workflow.rb +66 -0
  81. data/lib/DhanHQ/skills.rb +29 -0
  82. data/lib/DhanHQ/strategy/base.rb +189 -0
  83. data/lib/DhanHQ/strategy.rb +40 -0
  84. data/lib/DhanHQ/version.rb +1 -1
  85. data/lib/DhanHQ/ws/decoder.rb +57 -19
  86. data/lib/DhanHQ.rb +3 -0
  87. data/lib/dhan_hq/agent.rb +3 -0
  88. data/lib/dhan_hq/mcp.rb +3 -0
  89. data/lib/dhan_hq.rb +74 -2
  90. data/lib/ta/technical_analysis.rb +3 -1
  91. data/skills/dhanhq-ruby/SKILL.md +207 -0
  92. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  93. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  94. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  95. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  96. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  97. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  98. data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
  99. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  100. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  101. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  102. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  103. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  104. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  105. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  106. data/skills/dhanhq-ruby/references/funds.md +67 -0
  107. data/skills/dhanhq-ruby/references/instruments.md +85 -0
  108. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  109. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  110. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  111. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  112. data/skills/dhanhq-ruby/references/orders.md +201 -0
  113. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  114. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  115. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  116. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  117. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  118. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  119. 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
@@ -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
@@ -91,7 +91,6 @@ module DhanHQ
91
91
  def self.build_connection(base_url)
92
92
  Faraday.new(url: base_url) do |c|
93
93
  c.request :url_encoded
94
- c.response :json, content_type: /\bjson$/
95
94
  c.adapter Faraday.default_adapter
96
95
  end
97
96
  end
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 :json, parser_options: { symbolize_names: true }
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 and live trading safety.
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 = {})