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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +7 -0
  3. data/lib/DhanHQ/agent/order_preview.rb +50 -0
  4. data/lib/DhanHQ/agent/policy.rb +51 -0
  5. data/lib/DhanHQ/agent/tool_registry.rb +250 -0
  6. data/lib/DhanHQ/agent.rb +12 -0
  7. data/lib/DhanHQ/ai/context_builder.rb +145 -0
  8. data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
  9. data/lib/DhanHQ/ai.rb +27 -0
  10. data/lib/DhanHQ/auth.rb +0 -1
  11. data/lib/DhanHQ/client.rb +1 -3
  12. data/lib/DhanHQ/constants.rb +2 -0
  13. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
  14. data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
  15. data/lib/DhanHQ/core/auth_api.rb +0 -1
  16. data/lib/DhanHQ/errors.rb +4 -0
  17. data/lib/DhanHQ/events/base.rb +203 -0
  18. data/lib/DhanHQ/events/bus.rb +158 -0
  19. data/lib/DhanHQ/events.rb +40 -0
  20. data/lib/DhanHQ/indicators.rb +283 -0
  21. data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
  22. data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
  23. data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
  24. data/lib/DhanHQ/market_data.rb +25 -0
  25. data/lib/DhanHQ/mcp/server.rb +72 -0
  26. data/lib/DhanHQ/mcp.rb +10 -0
  27. data/lib/DhanHQ/models/funds.rb +12 -0
  28. data/lib/DhanHQ/models/holding.rb +42 -0
  29. data/lib/DhanHQ/models/iceberg_order.rb +139 -0
  30. data/lib/DhanHQ/models/instrument.rb +36 -0
  31. data/lib/DhanHQ/models/order.rb +95 -0
  32. data/lib/DhanHQ/models/position.rb +66 -0
  33. data/lib/DhanHQ/models/search_result.rb +12 -0
  34. data/lib/DhanHQ/models/trade.rb +13 -0
  35. data/lib/DhanHQ/models/twap_order.rb +136 -0
  36. data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
  37. data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
  38. data/lib/DhanHQ/option_analytics.rb +36 -0
  39. data/lib/DhanHQ/resources/iceberg_orders.rb +61 -0
  40. data/lib/DhanHQ/resources/twap_orders.rb +61 -0
  41. data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
  42. data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
  43. data/lib/DhanHQ/risk/checks/options.rb +46 -0
  44. data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
  45. data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
  46. data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
  47. data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
  48. data/lib/DhanHQ/risk/pipeline.rb +65 -0
  49. data/lib/DhanHQ/risk.rb +250 -0
  50. data/lib/DhanHQ/skills/base.rb +132 -0
  51. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +87 -0
  52. data/lib/DhanHQ/skills/builtin/iron_condor.rb +93 -0
  53. data/lib/DhanHQ/skills/builtin/square_off_all.rb +45 -0
  54. data/lib/DhanHQ/skills/builtin/square_off_position.rb +48 -0
  55. data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
  56. data/lib/DhanHQ/skills/registry.rb +101 -0
  57. data/lib/DhanHQ/skills/workflow.rb +66 -0
  58. data/lib/DhanHQ/skills.rb +29 -0
  59. data/lib/DhanHQ/strategy/base.rb +189 -0
  60. data/lib/DhanHQ/strategy.rb +40 -0
  61. data/lib/DhanHQ/version.rb +1 -1
  62. data/lib/DhanHQ/ws/decoder.rb +57 -19
  63. data/lib/DhanHQ.rb +3 -0
  64. data/lib/dhan_hq/agent.rb +3 -0
  65. data/lib/dhan_hq/mcp.rb +3 -0
  66. data/lib/dhan_hq.rb +27 -2
  67. data/lib/ta/technical_analysis.rb +3 -1
  68. data/skills/dhanhq-ruby/SKILL.md +74 -0
  69. data/skills/dhanhq-ruby/references/market_data.md +3 -0
  70. data/skills/dhanhq-ruby/references/orders.md +7 -0
  71. metadata +59 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf675947ad1be99c9af911e63ded9e131e85eec23703f9de3540c30a69b27749
4
- data.tar.gz: 2248c7ea6c009e0017327d3483c2bf13354a4994bf7a4fc63b76519f44e93b07
3
+ metadata.gz: 3d9d117dd455fc2a006a98cf0532297688a247498f5a12e98fb609a355ffc195
4
+ data.tar.gz: 2334b0e16f35ecc00f414739f3d40da1cac7640ede1b64079248b35e02cc0851
5
5
  SHA512:
6
- metadata.gz: e485bf1b92f5de3277b6cc611cc7f29035f70ccbea9c59bd8945c456382e5bc415795ba7d60f5d69b193c3c2d270a9cd2ed53502870fed6a4731e0b7fc2ac94d
7
- data.tar.gz: eb094317cddd3d927bfbd54a8b0b8598b367217cf67c0a8fc1000d0f5258bd6009007bb6be30b36868746f1fa2b22037fad4a9ad872f3ca0b294bad388656b32
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
@@ -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