ollama-client 0.2.5 → 0.2.6

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +138 -76
  4. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  5. data/docs/GETTING_STARTED.md +361 -0
  6. data/docs/INTEGRATION_TESTING.md +170 -0
  7. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  8. data/docs/PERSONAS.md +383 -0
  9. data/docs/QUICK_START.md +195 -0
  10. data/docs/TESTING.md +392 -170
  11. data/docs/TEST_CHECKLIST.md +450 -0
  12. data/examples/README.md +51 -66
  13. data/examples/basic_chat.rb +33 -0
  14. data/examples/basic_generate.rb +29 -0
  15. data/examples/tool_calling_parsing.rb +59 -0
  16. data/exe/ollama-client +128 -1
  17. data/lib/ollama/agent/planner.rb +7 -2
  18. data/lib/ollama/chat_session.rb +101 -0
  19. data/lib/ollama/client.rb +41 -35
  20. data/lib/ollama/config.rb +4 -1
  21. data/lib/ollama/document_loader.rb +1 -1
  22. data/lib/ollama/embeddings.rb +41 -26
  23. data/lib/ollama/errors.rb +1 -0
  24. data/lib/ollama/personas.rb +287 -0
  25. data/lib/ollama/version.rb +1 -1
  26. data/lib/ollama_client.rb +7 -0
  27. metadata +14 -48
  28. data/examples/advanced_complex_schemas.rb +0 -366
  29. data/examples/advanced_edge_cases.rb +0 -241
  30. data/examples/advanced_error_handling.rb +0 -200
  31. data/examples/advanced_multi_step_agent.rb +0 -341
  32. data/examples/advanced_performance_testing.rb +0 -186
  33. data/examples/chat_console.rb +0 -143
  34. data/examples/complete_workflow.rb +0 -245
  35. data/examples/dhan_console.rb +0 -843
  36. data/examples/dhanhq/README.md +0 -236
  37. data/examples/dhanhq/agents/base_agent.rb +0 -74
  38. data/examples/dhanhq/agents/data_agent.rb +0 -66
  39. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  40. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  41. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  42. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  43. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  44. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  45. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  46. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  47. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  48. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  49. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  50. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  51. data/examples/dhanhq/services/base_service.rb +0 -46
  52. data/examples/dhanhq/services/data_service.rb +0 -118
  53. data/examples/dhanhq/services/trading_service.rb +0 -59
  54. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  55. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  56. data/examples/dhanhq/test_tool_calling.rb +0 -538
  57. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  58. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  59. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  60. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  61. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  62. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  63. data/examples/dhanhq_agent.rb +0 -964
  64. data/examples/dhanhq_tools.rb +0 -1663
  65. data/examples/multi_step_agent_with_external_data.rb +0 -368
  66. data/examples/structured_outputs_chat.rb +0 -72
  67. data/examples/structured_tools.rb +0 -89
  68. data/examples/test_dhanhq_tool_calling.rb +0 -375
  69. data/examples/test_tool_calling.rb +0 -160
  70. data/examples/tool_calling_direct.rb +0 -124
  71. data/examples/tool_calling_pattern.rb +0 -269
  72. data/exe/dhan_console +0 -4
@@ -1,1663 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # DhanHQ Tools - All DhanHQ API operations
5
- # Contains:
6
- # - Data APIs (6): Market Quote, Live Market Feed, Full Market Depth,
7
- # Historical Data, Expired Options Data, Option Chain
8
- # - Trading Tools: Order parameter building (does not place orders)
9
-
10
- require "json"
11
- require "date"
12
- require "dhan_hq"
13
- require_relative "dhanhq/indicators/technical_indicators"
14
-
15
- # Helper to get valid exchange segments from DhanHQ constants
16
- def valid_exchange_segments
17
- DhanHQ::Constants::EXCHANGE_SEGMENTS
18
- rescue StandardError
19
- ["NSE_EQ", "NSE_FNO", "NSE_CURRENCY", "BSE_EQ", "BSE_FNO",
20
- "BSE_CURRENCY", "MCX_COMM", "IDX_I"]
21
- end
22
-
23
- # Helper to get INDEX constant
24
- def index_exchange_segment
25
- DhanHQ::Constants::INDEX
26
- rescue StandardError
27
- "IDX_I"
28
- end
29
-
30
- # Helper to extract values from different data structures
31
- def extract_value(data, keys)
32
- return nil unless data
33
-
34
- keys.each do |key|
35
- if data.is_a?(Hash)
36
- return data[key] if data.key?(key)
37
- elsif data.respond_to?(key)
38
- return data.send(key)
39
- end
40
- end
41
-
42
- # If data is a simple value and we're looking for it directly
43
- return data if data.is_a?(Numeric) || data.is_a?(String)
44
-
45
- nil
46
- end
47
-
48
- # Helper to safely get instrument attribute (handles missing methods)
49
- def safe_instrument_attr(instrument, attr_name)
50
- return nil unless instrument
51
-
52
- instrument.respond_to?(attr_name) ? instrument.send(attr_name) : nil
53
- rescue StandardError
54
- nil
55
- end
56
-
57
- # Helper to validate security_id is numeric (integer or numeric string), not a symbol string
58
- # Returns [is_valid, result_or_error_message]
59
- # If valid: [true, integer_value]
60
- # If invalid: [false, error_message_string]
61
- def validate_security_id_numeric(security_id)
62
- return [false, "security_id cannot be nil"] if security_id.nil?
63
-
64
- # If it's already an integer, it's valid
65
- return [true, security_id.to_i] if security_id.is_a?(Integer)
66
-
67
- # If it's a string, check if it's numeric
68
- if security_id.is_a?(String)
69
- # Remove whitespace
70
- cleaned = security_id.strip
71
-
72
- # Check if it's a numeric string (all digits, possibly with leading/trailing spaces)
73
- return [true, cleaned.to_i] if cleaned.match?(/^\d+$/)
74
-
75
- # It's a non-numeric string (likely a symbol like "NIFTY")
76
- message = "security_id must be numeric (integer or numeric string like '13'), " \
77
- "not a symbol string like '#{cleaned}'. Use symbol parameter for symbols."
78
- return [false, message]
79
-
80
- end
81
-
82
- # Try to convert to integer
83
- begin
84
- int_value = security_id.to_i
85
- # Check if conversion was successful (to_i returns 0 for non-numeric strings)
86
- if int_value.zero? && security_id.to_s.strip != "0"
87
- return [false, "security_id must be numeric (integer or numeric string), got: #{security_id.inspect}"]
88
- end
89
-
90
- [true, int_value]
91
- rescue StandardError
92
- [false, "security_id must be numeric (integer or numeric string), got: #{security_id.inspect}"]
93
- end
94
- end
95
-
96
- # Debug logging helper (if needed)
97
- def debug_log(location, message, data = {}, hypothesis_id = nil)
98
- log_entry = {
99
- sessionId: "debug-session",
100
- runId: "run1",
101
- hypothesisId: hypothesis_id,
102
- location: location,
103
- message: message,
104
- data: data,
105
- timestamp: Time.now.to_f * 1000
106
- }
107
- File.open("/home/nemesis/project/ollama-client/.cursor/debug.log", "a") do |f|
108
- f.puts(log_entry.to_json)
109
- end
110
- rescue StandardError
111
- # Ignore logging errors
112
- end
113
-
114
- # DhanHQ Data Tools - Data APIs only
115
- # Contains only the 6 Data APIs:
116
- # 1. Market Quote
117
- # 2. Live Market Feed (LTP)
118
- # 3. Full Market Depth
119
- # 4. Historical Data
120
- # 5. Expired Options Data
121
- # 6. Option Chain
122
- #
123
- # NOTE: These tools are callable functions. For use with Ollama::Agent::Executor,
124
- # you can either:
125
- # 1. Use them directly as callables (auto-inferred schema)
126
- # 2. Wrap them with structured Ollama::Tool classes for explicit schemas
127
- # See examples/dhanhq/technical_analysis_agentic_runner.rb for examples
128
- # of structured Tool definitions with type safety and better LLM understanding.
129
- class DhanHQDataTools
130
- class << self
131
- # Rate limiting: MarketFeed APIs have a limit of 1 request per second
132
- # Track last API call time to enforce rate limiting
133
- @last_marketfeed_call = nil
134
- @marketfeed_mutex = Mutex.new
135
-
136
- # Helper to enforce rate limiting for MarketFeed APIs (1 request per second)
137
- def rate_limit_marketfeed
138
- @marketfeed_mutex ||= Mutex.new
139
- @marketfeed_mutex.synchronize do
140
- if @last_marketfeed_call
141
- elapsed = Time.now - @last_marketfeed_call
142
- sleep(1.1 - elapsed) if elapsed < 1.1 # Add 0.1s buffer
143
- end
144
- @last_marketfeed_call = Time.now
145
- end
146
- end
147
-
148
- # 0. Find Instrument - Search for instrument by symbol across common exchange segments
149
- #
150
- # REQUIRED PARAMETERS:
151
- # - symbol [String]: Trading symbol to search for (e.g., "NIFTY", "RELIANCE", "TCS")
152
- #
153
- # Returns: Instrument details including:
154
- # - exchange_segment: Exchange and segment identifier (e.g., "IDX_I", "NSE_EQ")
155
- # - security_id: Numeric security ID (use this for subsequent API calls)
156
- # - trading_symbol: Trading symbol
157
- # - instrument_type: Type of instrument (EQUITY, INDEX, etc.)
158
- # - name: Full name of the instrument
159
- # - isin: ISIN code if available
160
- #
161
- # This is useful when you only have a symbol and need to resolve it to proper parameters
162
- # Searches across common segments: NSE_EQ, BSE_EQ, NSE_FNO, BSE_FNO, IDX_I, NSE_CURRENCY, BSE_CURRENCY, MCX_COMM
163
- def find_instrument(symbol:)
164
- unless symbol
165
- return {
166
- action: "find_instrument",
167
- error: "Symbol is required",
168
- params: { symbol: symbol }
169
- }
170
- end
171
-
172
- symbol_str = symbol.to_s.upcase
173
- common_index_symbols = %w[NIFTY BANKNIFTY FINNIFTY MIDCPNIFTY SENSEX BANKEX]
174
-
175
- if common_index_symbols.include?(symbol_str)
176
- instrument = DhanHQ::Models::Instrument.find("IDX_I", symbol_str)
177
- if instrument
178
- return {
179
- action: "find_instrument",
180
- params: { symbol: symbol_str },
181
- result: {
182
- symbol: symbol_str,
183
- exchange_segment: "IDX_I",
184
- security_id: safe_instrument_attr(instrument, :security_id),
185
- trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
186
- instrument_type: safe_instrument_attr(instrument, :instrument_type),
187
- name: safe_instrument_attr(instrument, :name),
188
- isin: safe_instrument_attr(instrument, :isin)
189
- }
190
- }
191
- end
192
- end
193
-
194
- common_segments = %w[NSE_EQ BSE_EQ NSE_FNO BSE_FNO IDX_I NSE_CURRENCY BSE_CURRENCY MCX_COMM]
195
-
196
- common_segments.each do |segment|
197
- instrument = DhanHQ::Models::Instrument.find(segment, symbol_str)
198
- next unless instrument
199
-
200
- return {
201
- action: "find_instrument",
202
- params: { symbol: symbol_str },
203
- result: {
204
- symbol: symbol_str,
205
- exchange_segment: segment,
206
- security_id: safe_instrument_attr(instrument, :security_id),
207
- trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
208
- instrument_type: safe_instrument_attr(instrument, :instrument_type),
209
- name: safe_instrument_attr(instrument, :name),
210
- isin: safe_instrument_attr(instrument, :isin)
211
- }
212
- }
213
- end
214
-
215
- {
216
- action: "find_instrument",
217
- error: "Instrument not found in any common exchange segment",
218
- params: { symbol: symbol_str },
219
- suggestions: "Try specifying exchange_segment explicitly (NSE_EQ, BSE_EQ, etc.)"
220
- }
221
- rescue StandardError => e
222
- {
223
- action: "find_instrument",
224
- error: e.message,
225
- params: { symbol: symbol }
226
- }
227
- end
228
-
229
- # 1. Market Quote API - Get market quote using Instrument convenience method
230
- #
231
- # REQUIRED PARAMETERS:
232
- # - exchange_segment [String]: Exchange and segment identifier
233
- # Valid values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
234
- # - symbol [String] OR security_id [Integer]: Must provide one
235
- # symbol: Trading symbol (e.g., "NIFTY", "RELIANCE")
236
- # security_id: Numeric security ID
237
- #
238
- # Returns: Full market quote with OHLC, depth, volume, and other market data
239
- #
240
- # Rate limit: 1 request per second
241
- # Up to 1000 instruments per request
242
- #
243
- # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
244
- def get_market_quote(exchange_segment:, security_id: nil, symbol: nil)
245
- error = require_symbol_or_security_id(action: "get_market_quote",
246
- exchange_segment: exchange_segment,
247
- security_id: security_id,
248
- symbol: symbol)
249
- return error if error
250
-
251
- rate_limit_marketfeed
252
- exchange_segment = exchange_segment.to_s
253
-
254
- if security_id && !symbol
255
- error, security_id_int = validated_security_id_for_marketfeed(
256
- action: "get_market_quote",
257
- exchange_segment: exchange_segment,
258
- security_id: security_id,
259
- symbol: symbol
260
- )
261
- return error if error
262
-
263
- return market_quote_from_security_id(exchange_segment: exchange_segment,
264
- security_id_int: security_id_int,
265
- symbol: symbol)
266
- end
267
-
268
- market_quote_from_symbol(exchange_segment: exchange_segment, security_id: security_id, symbol: symbol)
269
- rescue StandardError => e
270
- action_error(action: "get_market_quote",
271
- message: e.message,
272
- exchange_segment: exchange_segment,
273
- security_id: security_id,
274
- symbol: symbol)
275
- end
276
-
277
- # 2. Live Market Feed API - Get LTP (Last Traded Price) using Instrument convenience method
278
- #
279
- # REQUIRED PARAMETERS:
280
- # - exchange_segment [String]: Exchange and segment identifier
281
- # Valid values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
282
- # - symbol [String] OR security_id [Integer]: Must provide one
283
- # symbol: Trading symbol (e.g., "NIFTY", "RELIANCE")
284
- # security_id: Numeric security ID
285
- #
286
- # Returns: Last Traded Price (LTP) - fastest API for current price
287
- #
288
- # Rate limit: 1 request per second
289
- # Up to 1000 instruments per request
290
- #
291
- # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
292
- def get_live_ltp(exchange_segment:, security_id: nil, symbol: nil)
293
- error = require_symbol_or_security_id(action: "get_live_ltp",
294
- exchange_segment: exchange_segment,
295
- security_id: security_id,
296
- symbol: symbol)
297
- return error if error
298
-
299
- rate_limit_marketfeed
300
- exchange_segment = exchange_segment.to_s
301
-
302
- if security_id && !symbol
303
- error, security_id_int = validated_security_id_for_marketfeed(
304
- action: "get_live_ltp",
305
- exchange_segment: exchange_segment,
306
- security_id: security_id,
307
- symbol: symbol
308
- )
309
- return error if error
310
-
311
- ltp, ltp_data = ltp_from_marketfeed(exchange_segment: exchange_segment, security_id: security_id_int)
312
- return live_ltp_response(exchange_segment: exchange_segment,
313
- security_id: security_id_int,
314
- symbol: symbol,
315
- ltp_payload: { ltp: ltp, ltp_data: ltp_data })
316
- end
317
-
318
- instrument_symbol = symbol.to_s
319
- instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
320
- unless instrument
321
- return action_error(action: "get_live_ltp",
322
- message: "Instrument not found",
323
- exchange_segment: exchange_segment,
324
- security_id: security_id,
325
- symbol: symbol)
326
- end
327
-
328
- ltp, ltp_data, resolved_security_id = ltp_from_instrument(exchange_segment: exchange_segment,
329
- instrument: instrument,
330
- security_id: security_id)
331
- live_ltp_response(exchange_segment: exchange_segment,
332
- security_id: resolved_security_id,
333
- symbol: symbol,
334
- ltp_payload: { ltp: ltp, ltp_data: ltp_data },
335
- instrument_symbol: instrument_symbol)
336
- rescue StandardError => e
337
- action_error(action: "get_live_ltp",
338
- message: e.message,
339
- exchange_segment: exchange_segment,
340
- security_id: security_id,
341
- symbol: symbol)
342
- end
343
-
344
- # 3. Full Market Depth API - Get full market depth (bid/ask levels)
345
- #
346
- # REQUIRED PARAMETERS:
347
- # - exchange_segment [String]: Exchange and segment identifier
348
- # Valid values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
349
- # - symbol [String] OR security_id [Integer]: Must provide one
350
- # symbol: Trading symbol (e.g., "NIFTY", "RELIANCE")
351
- # security_id: Numeric security ID
352
- #
353
- # Returns: Full market depth with order book (bid/ask levels), OHLC, volume, OI
354
- #
355
- # Rate limit: 1 request per second (uses quote API which has stricter limits)
356
- # Up to 1000 instruments per request
357
- #
358
- # Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
359
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
360
- def get_market_depth(exchange_segment:, security_id: nil, symbol: nil)
361
- # CRITICAL: security_id must be an integer, not a symbol string
362
- unless symbol || security_id
363
- return {
364
- action: "get_market_depth",
365
- error: "Either symbol or security_id must be provided",
366
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
367
- }
368
- end
369
-
370
- rate_limit_marketfeed # Enforce rate limiting
371
- exchange_segment = exchange_segment.to_s
372
-
373
- # If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
374
- if security_id && !symbol
375
- is_valid, result = validate_security_id_numeric(security_id)
376
- unless is_valid
377
- return {
378
- action: "get_market_depth",
379
- error: result,
380
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
381
- }
382
- end
383
- security_id_int = result
384
- unless security_id_int.positive?
385
- return {
386
- action: "get_market_depth",
387
- error: "security_id must be a positive integer, got: #{security_id.inspect}",
388
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
389
- }
390
- end
391
-
392
- # Use MarketFeed.quote directly with security_id
393
- payload = { exchange_segment => [security_id_int] }
394
- quote_response = DhanHQ::Models::MarketFeed.quote(payload)
395
-
396
- if quote_response.is_a?(Hash) && quote_response["data"]
397
- quote_data = quote_response.dig("data", exchange_segment, security_id_int.to_s)
398
- end
399
-
400
- depth = extract_value(quote_data, [:depth, "depth"]) if quote_data
401
- buy_depth = extract_value(depth, [:buy, "buy"]) if depth
402
- sell_depth = extract_value(depth, [:sell, "sell"]) if depth
403
-
404
- return {
405
- action: "get_market_depth",
406
- params: { security_id: security_id_int, symbol: symbol, exchange_segment: exchange_segment },
407
- result: {
408
- security_id: security_id_int,
409
- exchange_segment: exchange_segment,
410
- market_depth: quote_data || quote_response,
411
- buy_depth: buy_depth,
412
- sell_depth: sell_depth,
413
- ltp: quote_data ? extract_value(quote_data, [:last_price, "last_price"]) : nil,
414
- volume: quote_data ? extract_value(quote_data, [:volume, "volume"]) : nil,
415
- oi: quote_data ? extract_value(quote_data, [:oi, "oi"]) : nil,
416
- ohlc: quote_data ? extract_value(quote_data, [:ohlc, "ohlc"]) : nil
417
- }
418
- }
419
- end
420
-
421
- # Find instrument using symbol (Instrument.find expects symbol, not security_id)
422
- instrument_symbol = symbol.to_s
423
- instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
424
-
425
- if instrument
426
- # Use instrument convenience method - automatically uses instrument's attributes
427
- # Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
428
- quote_response = instrument.quote
429
-
430
- # Extract actual quote data from nested structure
431
- security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
432
- if quote_response.is_a?(Hash) && quote_response["data"]
433
- quote_data = quote_response.dig("data", exchange_segment,
434
- security_id_str)
435
- end
436
-
437
- # Extract market depth (order book) from quote data
438
- depth = extract_value(quote_data, [:depth, "depth"]) if quote_data
439
- buy_depth = extract_value(depth, [:buy, "buy"]) if depth
440
- sell_depth = extract_value(depth, [:sell, "sell"]) if depth
441
-
442
- {
443
- action: "get_market_depth",
444
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
445
- result: {
446
- security_id: safe_instrument_attr(instrument, :security_id) || security_id,
447
- symbol: instrument_symbol,
448
- exchange_segment: exchange_segment,
449
- market_depth: quote_data || quote_response,
450
- # Market depth (order book) - buy and sell sides
451
- buy_depth: buy_depth,
452
- sell_depth: sell_depth,
453
- # Additional quote data
454
- ltp: quote_data ? extract_value(quote_data, [:last_price, "last_price"]) : nil,
455
- volume: quote_data ? extract_value(quote_data, [:volume, "volume"]) : nil,
456
- oi: quote_data ? extract_value(quote_data, [:oi, "oi"]) : nil,
457
- ohlc: quote_data ? extract_value(quote_data, [:ohlc, "ohlc"]) : nil
458
- }
459
- }
460
- else
461
- {
462
- action: "get_market_depth",
463
- error: "Instrument not found",
464
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
465
- }
466
- end
467
- rescue StandardError => e
468
- {
469
- action: "get_market_depth",
470
- error: e.message,
471
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
472
- }
473
- end
474
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
475
-
476
- # 4. Historical Data API - Get historical data using HistoricalData class directly
477
- #
478
- # REQUIRED PARAMETERS:
479
- # - exchange_segment [String]: Exchange and segment identifier
480
- # Valid values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
481
- # - from_date [String]: Start date in YYYY-MM-DD format (e.g., "2024-01-08")
482
- # - to_date [String]: End date (non-inclusive) in YYYY-MM-DD format (e.g., "2024-02-08")
483
- # - symbol [String] OR security_id [Integer]: Must provide one
484
- # symbol: Trading symbol (e.g., "NIFTY", "RELIANCE")
485
- # security_id: Numeric security ID
486
- #
487
- # OPTIONAL PARAMETERS:
488
- # - interval [String]: Minute intervals for intraday data
489
- # Valid values: "1", "5", "15", "25", "60"
490
- # If provided, returns intraday data; if omitted, returns daily data
491
- # - expiry_code [Integer]: Expiry code for derivatives
492
- # Valid values: 0 (far month), 1 (near month), 2 (current month)
493
- # - instrument [String]: Instrument type
494
- # Valid values: EQUITY, INDEX, FUTIDX, FUTSTK, OPTIDX, OPTSTK, FUTCOM, OPTFUT, FUTCUR, OPTCUR
495
- # Auto-detected from exchange_segment if not provided
496
- #
497
- # Returns: Historical OHLCV data (arrays of open, high, low, close, volume, timestamp)
498
- #
499
- # Notes:
500
- # - Maximum 90 days of data for intraday requests
501
- # - Historical data available for the last 5 years
502
- # - to_date is NON-INCLUSIVE (end date is not included in results)
503
- #
504
- # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
505
- def get_historical_data(exchange_segment:, from_date:, to_date:, security_id: nil, symbol: nil, interval: nil,
506
- expiry_code: nil, instrument: nil, calculate_indicators: false)
507
- # CRITICAL: security_id must be an integer, not a symbol string
508
- unless symbol || security_id
509
- return {
510
- action: "get_historical_data",
511
- error: "Either symbol or security_id must be provided",
512
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
513
- }
514
- end
515
-
516
- exchange_segment = exchange_segment.to_s
517
-
518
- # If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
519
- # If symbol is provided, find instrument first to get security_id and instrument type
520
- if security_id && !symbol
521
- is_valid, result = validate_security_id_numeric(security_id)
522
- unless is_valid
523
- return {
524
- action: "get_historical_data",
525
- error: result,
526
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
527
- }
528
- end
529
- security_id_int = result
530
- unless security_id_int.positive?
531
- return {
532
- action: "get_historical_data",
533
- error: "security_id must be a positive integer, got: #{security_id.inspect}",
534
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
535
- }
536
- end
537
- resolved_security_id = security_id_int.to_s
538
- found_instrument = nil
539
- else
540
- # Find instrument using symbol (Instrument.find expects symbol, not security_id)
541
- instrument_symbol = symbol.to_s
542
- found_instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
543
- unless found_instrument
544
- return {
545
- action: "get_historical_data",
546
- error: "Instrument not found",
547
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
548
- }
549
- end
550
-
551
- # Get security_id from instrument
552
- resolved_security_id = safe_instrument_attr(found_instrument, :security_id)
553
- unless resolved_security_id
554
- return {
555
- action: "get_historical_data",
556
- error: "security_id is required and could not be determined from instrument",
557
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
558
- }
559
- end
560
- resolved_security_id = resolved_security_id.to_s
561
- end
562
-
563
- # Determine instrument type if not provided
564
- # Valid instrument types: INDEX, FUTIDX, OPTIDX, EQUITY, FUTSTK, OPTSTK, FUTCOM, OPTFUT, FUTCUR, OPTCUR
565
- valid_instruments = %w[INDEX FUTIDX OPTIDX EQUITY FUTSTK OPTSTK FUTCOM OPTFUT FUTCUR OPTCUR]
566
-
567
- # Try to get from instrument object first, validate it
568
- instrument_type_from_obj = safe_instrument_attr(found_instrument, :instrument_type)
569
- instrument_type_from_obj = instrument_type_from_obj.to_s.upcase if instrument_type_from_obj
570
- instrument_type_from_obj = nil unless valid_instruments.include?(instrument_type_from_obj)
571
-
572
- # Determine instrument type if not provided
573
- resolved_instrument = if found_instrument
574
- instrument_type_from_obj || default_instrument_for(exchange_segment)
575
- else
576
- instrument&.to_s&.upcase || default_instrument_for(exchange_segment)
577
- end
578
-
579
- # Final validation - ensure the resolved instrument type is valid
580
- resolved_instrument = if valid_instruments.include?(resolved_instrument.to_s.upcase)
581
- resolved_instrument.to_s.upcase
582
- else
583
- # Fallback to EQUITY if invalid
584
- "EQUITY"
585
- end
586
-
587
- if interval
588
- # Intraday data using HistoricalData.intraday
589
- # Returns hash with :open, :high, :low, :close, :volume, :timestamp arrays
590
- intraday_params = {
591
- security_id: resolved_security_id,
592
- exchange_segment: exchange_segment,
593
- instrument: resolved_instrument,
594
- interval: interval.to_s,
595
- from_date: from_date,
596
- to_date: to_date
597
- }
598
- intraday_params[:expiry_code] = expiry_code if expiry_code
599
- data = DhanHQ::Models::HistoricalData.intraday(intraday_params)
600
-
601
- # Count is based on the length of one of the arrays (e.g., :open or :close)
602
- count = if data.is_a?(Hash)
603
- data[:open]&.length || data[:close]&.length || data["open"]&.length || data["close"]&.length || 0
604
- else
605
- 0
606
- end
607
-
608
- # If calculate_indicators is true, calculate technical indicators and return only those
609
- if calculate_indicators && data.is_a?(Hash) && count.positive?
610
- indicators = calculate_technical_indicators(data)
611
- {
612
- action: "get_historical_data",
613
- type: "intraday",
614
- params: {
615
- security_id: resolved_security_id,
616
- symbol: symbol,
617
- exchange_segment: exchange_segment,
618
- instrument: resolved_instrument,
619
- from_date: from_date,
620
- to_date: to_date,
621
- interval: interval
622
- },
623
- result: {
624
- indicators: indicators,
625
- data_points: count,
626
- note: "Technical indicators calculated from historical data. " \
627
- "Raw data not included to reduce response size.",
628
- instrument_info: {
629
- security_id: resolved_security_id,
630
- trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
631
- instrument_type: resolved_instrument
632
- }
633
- }
634
- }
635
- else
636
- {
637
- action: "get_historical_data",
638
- type: "intraday",
639
- params: {
640
- security_id: resolved_security_id,
641
- symbol: symbol,
642
- exchange_segment: exchange_segment,
643
- instrument: resolved_instrument,
644
- from_date: from_date,
645
- to_date: to_date,
646
- interval: interval
647
- },
648
- result: {
649
- data: data,
650
- count: count,
651
- instrument_info: {
652
- security_id: resolved_security_id,
653
- trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
654
- instrument_type: resolved_instrument
655
- }
656
- }
657
- }
658
- end
659
- else
660
- # Daily data using HistoricalData.daily
661
- # Returns hash with :open, :high, :low, :close, :volume, :timestamp arrays
662
- daily_params = {
663
- security_id: resolved_security_id,
664
- exchange_segment: exchange_segment,
665
- instrument: resolved_instrument,
666
- from_date: from_date,
667
- to_date: to_date
668
- }
669
- daily_params[:expiry_code] = expiry_code if expiry_code
670
- data = DhanHQ::Models::HistoricalData.daily(daily_params)
671
-
672
- # Count is based on the length of one of the arrays (e.g., :open or :close)
673
- count = if data.is_a?(Hash)
674
- data[:open]&.length || data[:close]&.length || data["open"]&.length || data["close"]&.length || 0
675
- else
676
- 0
677
- end
678
-
679
- # If calculate_indicators is true, calculate technical indicators and return only those
680
- if calculate_indicators && data.is_a?(Hash) && count.positive?
681
- indicators = calculate_technical_indicators(data)
682
- {
683
- action: "get_historical_data",
684
- type: "daily",
685
- params: {
686
- security_id: resolved_security_id,
687
- symbol: symbol,
688
- exchange_segment: exchange_segment,
689
- instrument: resolved_instrument,
690
- from_date: from_date,
691
- to_date: to_date,
692
- expiry_code: expiry_code
693
- },
694
- result: {
695
- indicators: indicators,
696
- data_points: count,
697
- note: "Technical indicators calculated from historical data. " \
698
- "Raw data not included to reduce response size.",
699
- instrument_info: {
700
- security_id: resolved_security_id,
701
- trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
702
- instrument_type: resolved_instrument
703
- }
704
- }
705
- }
706
- else
707
- {
708
- action: "get_historical_data",
709
- type: "daily",
710
- params: {
711
- security_id: resolved_security_id,
712
- symbol: symbol,
713
- exchange_segment: exchange_segment,
714
- instrument: resolved_instrument,
715
- from_date: from_date,
716
- to_date: to_date,
717
- expiry_code: expiry_code
718
- },
719
- result: {
720
- data: data,
721
- count: count,
722
- instrument_info: {
723
- security_id: resolved_security_id,
724
- trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
725
- instrument_type: resolved_instrument
726
- }
727
- }
728
- }
729
- end
730
- end
731
- rescue StandardError => e
732
- {
733
- action: "get_historical_data",
734
- error: e.message,
735
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
736
- }
737
- end
738
-
739
- def default_instrument_for(exchange_segment)
740
- defaults = {
741
- "IDX_I" => "INDEX",
742
- "NSE_FNO" => "FUTIDX",
743
- "BSE_FNO" => "FUTIDX",
744
- "NSE_CURRENCY" => "FUTCUR",
745
- "BSE_CURRENCY" => "FUTCUR",
746
- "MCX_COMM" => "FUTCOM"
747
- }
748
-
749
- defaults.fetch(exchange_segment, "EQUITY")
750
- end
751
-
752
- # Calculate technical indicators from historical data
753
- # Returns only indicator values, not raw data
754
- def calculate_technical_indicators(data)
755
- return {} unless data.is_a?(Hash)
756
-
757
- # Extract OHLCV arrays (handle both symbol and string keys)
758
- highs = data[:high] || data["high"] || []
759
- lows = data[:low] || data["low"] || []
760
- closes = data[:close] || data["close"] || []
761
- volumes = data[:volume] || data["volume"] || []
762
- timestamps = data[:timestamp] || data["timestamp"] || []
763
-
764
- return {} if closes.empty?
765
-
766
- # Convert to arrays of floats
767
- closes_f = closes.map(&:to_f)
768
- highs_f = highs.map(&:to_f)
769
- lows_f = lows.map(&:to_f)
770
- volumes_f = volumes.map(&:to_f)
771
-
772
- # Calculate indicators
773
- sma20 = DhanHQ::Indicators::TechnicalIndicators.sma(closes_f, 20)
774
- sma50 = DhanHQ::Indicators::TechnicalIndicators.sma(closes_f, 50)
775
- ema12 = DhanHQ::Indicators::TechnicalIndicators.ema(closes_f, 12)
776
- ema26 = DhanHQ::Indicators::TechnicalIndicators.ema(closes_f, 26)
777
- rsi = DhanHQ::Indicators::TechnicalIndicators.rsi(closes_f, 14)
778
- macd = DhanHQ::Indicators::TechnicalIndicators.macd(closes_f)
779
- bollinger = DhanHQ::Indicators::TechnicalIndicators.bollinger_bands(closes_f, 20, 2)
780
- atr = DhanHQ::Indicators::TechnicalIndicators.atr(highs_f, lows_f, closes_f, 14)
781
-
782
- # Get latest values
783
- {
784
- current_price: closes_f.last,
785
- sma20: sma20.last,
786
- sma50: sma50.last,
787
- ema12: ema12.last,
788
- ema26: ema26.last,
789
- rsi: rsi.last,
790
- macd: {
791
- macd: macd[:macd].last,
792
- signal: macd[:signal].last,
793
- histogram: macd[:histogram].last
794
- },
795
- bollinger_bands: {
796
- upper: bollinger[:upper].last,
797
- middle: bollinger[:middle].last,
798
- lower: bollinger[:lower].last
799
- },
800
- atr: atr.last,
801
- price_range: {
802
- high: highs_f.max,
803
- low: lows_f.min,
804
- current: closes_f.last
805
- },
806
- volume: {
807
- current: volumes_f.last,
808
- average: volumes_f.any? ? (volumes_f.sum.to_f / volumes_f.length) : nil
809
- },
810
- data_points: closes_f.length,
811
- last_timestamp: timestamps.last
812
- }
813
- rescue StandardError => e
814
- {
815
- error: "Failed to calculate indicators: #{e.message}"
816
- }
817
- end
818
- # rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
819
-
820
- # 6a. Option Expiry List API - Get list of available expiry dates for an underlying instrument
821
- #
822
- # REQUIRED PARAMETERS:
823
- # - exchange_segment [String]: Exchange and segment of underlying
824
- # For indices (NIFTY, BANKNIFTY): Use "IDX_I"
825
- # For stocks: Use "NSE_FNO" or "BSE_FNO"
826
- # Valid values: IDX_I (Index), NSE_FNO (NSE F&O), BSE_FNO (BSE F&O), MCX_FO (MCX)
827
- # - symbol [String] OR security_id [Integer]: Must provide one
828
- # symbol: Trading symbol (e.g., "NIFTY", "BANKNIFTY")
829
- # security_id: Numeric security ID of underlying (use value from find_instrument)
830
- #
831
- # Returns: Array of available expiry dates in "YYYY-MM-DD" format
832
- #
833
- # Rate limit: 1 request per 3 seconds
834
- #
835
- # Note: For index options, underlying_seg should be "IDX_I", not "NSE_FNO"
836
- def get_expiry_list(exchange_segment:, security_id: nil, symbol: nil)
837
- # CRITICAL: security_id must be an integer, not a symbol string
838
- unless symbol || security_id
839
- return {
840
- action: "get_expiry_list",
841
- error: "Either symbol or security_id must be provided",
842
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
843
- }
844
- end
845
-
846
- exchange_segment = exchange_segment.to_s
847
-
848
- # If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
849
- if security_id
850
- is_valid, result = validate_security_id_numeric(security_id)
851
- unless is_valid
852
- return {
853
- action: "get_expiry_list",
854
- error: result,
855
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
856
- }
857
- end
858
- security_id_int = result
859
- unless security_id_int.positive?
860
- return {
861
- action: "get_expiry_list",
862
- error: "security_id must be a positive integer, got: #{security_id.inspect}",
863
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
864
- }
865
- end
866
-
867
- underlying_seg = if exchange_segment == "IDX_I"
868
- "IDX_I"
869
- else
870
- exchange_segment
871
- end
872
- resolved_security_id = security_id_int
873
- found_instrument = nil
874
- elsif symbol
875
- instrument_symbol = symbol.to_s
876
- underlying_seg, found_instrument = find_underlying_instrument(exchange_segment, instrument_symbol)
877
- resolved_security_id = resolve_security_id_for_option_chain(nil, found_instrument)
878
- end
879
-
880
- unless resolved_security_id
881
- return {
882
- action: "get_expiry_list",
883
- error: "security_id is required and could not be determined from instrument",
884
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
885
- }
886
- end
887
-
888
- expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
889
- underlying_scrip: resolved_security_id,
890
- underlying_seg: underlying_seg
891
- )
892
-
893
- {
894
- action: "get_expiry_list",
895
- params: {
896
- security_id: resolved_security_id,
897
- symbol: symbol,
898
- exchange_segment: exchange_segment
899
- }.compact,
900
- result: {
901
- expiries: expiries,
902
- count: expiries.is_a?(Array) ? expiries.length : 0,
903
- instrument_info: {
904
- underlying_security_id: resolved_security_id,
905
- underlying_seg: underlying_seg,
906
- trading_symbol: found_instrument ? safe_instrument_attr(found_instrument, :trading_symbol) : symbol
907
- }
908
- }
909
- }
910
- rescue StandardError => e
911
- {
912
- action: "get_expiry_list",
913
- error: e.message,
914
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
915
- }
916
- end
917
-
918
- # 6. Option Chain API - Get option chain using OptionChain.fetch
919
- #
920
- # REQUIRED PARAMETERS:
921
- # - exchange_segment [String]: Exchange and segment of underlying
922
- # For indices (NIFTY, BANKNIFTY): Use "IDX_I"
923
- # For stocks: Use "NSE_FNO" or "BSE_FNO"
924
- # Valid values: IDX_I (Index), NSE_FNO (NSE F&O), BSE_FNO (BSE F&O), MCX_FO (MCX)
925
- # - symbol [String] OR security_id [Integer]: Must provide one
926
- # symbol: Trading symbol (e.g., "NIFTY", "BANKNIFTY")
927
- # security_id: Numeric security ID of underlying (use value from find_instrument)
928
- # - expiry [String]: Expiry date in YYYY-MM-DD format (REQUIRED)
929
- #
930
- # Returns: Option chain with CE/PE data for all strikes
931
- #
932
- # Rate limit: 1 request per 3 seconds
933
- # Automatically filters out strikes where both CE and PE have zero last_price
934
- #
935
- # Note: For index options, underlying_seg should be "IDX_I", not "NSE_FNO"
936
- # Note: Use get_expiry_list first to get available expiry dates if you don't know the expiry
937
- # Note: Chain is automatically filtered to show strikes around ATM (default: 5 strikes = 2 ITM, ATM, 2 OTM)
938
- def get_option_chain(exchange_segment:, security_id: nil, symbol: nil, expiry: nil, strikes_count: 5)
939
- # CRITICAL: security_id must be an integer, not a symbol string
940
- unless symbol || security_id
941
- return option_chain_error(
942
- "Either symbol or security_id must be provided",
943
- security_id: security_id,
944
- symbol: symbol,
945
- exchange_segment: exchange_segment
946
- )
947
- end
948
-
949
- exchange_segment = exchange_segment.to_s
950
-
951
- # If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
952
- # CRITICAL: Prefer security_id over symbol when both are provided, since security_id is more reliable
953
- if security_id
954
- is_valid, result = validate_security_id_numeric(security_id)
955
- unless is_valid
956
- return option_chain_error(
957
- result,
958
- security_id: security_id,
959
- symbol: symbol,
960
- exchange_segment: exchange_segment
961
- )
962
- end
963
- security_id_int = result
964
- unless security_id_int.positive?
965
- return option_chain_error(
966
- "security_id must be a positive integer, got: #{security_id.inspect}",
967
- security_id: security_id,
968
- symbol: symbol,
969
- exchange_segment: exchange_segment
970
- )
971
- end
972
-
973
- # For option chain, underlying_seg should be IDX_I for indices, or match exchange_segment
974
- # For indices like NIFTY (IDX_I), underlying_seg must be IDX_I
975
- underlying_seg = if exchange_segment == "IDX_I"
976
- "IDX_I"
977
- else
978
- exchange_segment
979
- end
980
- resolved_security_id = security_id_int
981
- found_instrument = nil
982
- elsif symbol
983
- # Find instrument using symbol (Instrument.find expects symbol, not security_id)
984
- instrument_symbol = symbol.to_s
985
- underlying_seg, found_instrument = find_underlying_instrument(exchange_segment, instrument_symbol)
986
-
987
- # Get security_id from instrument
988
- resolved_security_id = resolve_security_id_for_option_chain(nil, found_instrument)
989
- else
990
- return option_chain_error(
991
- "Either symbol or security_id must be provided",
992
- security_id: security_id,
993
- symbol: symbol,
994
- exchange_segment: exchange_segment
995
- )
996
- end
997
-
998
- unless resolved_security_id
999
- return option_chain_error(
1000
- "security_id is required and could not be determined from instrument",
1001
- security_id: security_id,
1002
- symbol: symbol,
1003
- exchange_segment: exchange_segment
1004
- )
1005
- end
1006
-
1007
- # Validate expiry is provided and not empty
1008
- expiry_str = expiry.to_s.strip if expiry
1009
- if expiry_str.nil? || expiry_str.empty?
1010
- return option_chain_error(
1011
- "expiry is required. Use get_expiry_list to get available expiry dates first.",
1012
- security_id: security_id,
1013
- symbol: symbol,
1014
- exchange_segment: exchange_segment,
1015
- expiry: expiry
1016
- )
1017
- end
1018
-
1019
- option_chain_for_expiry(
1020
- {
1021
- resolved_security_id: resolved_security_id,
1022
- underlying_seg: underlying_seg,
1023
- exchange_segment: exchange_segment,
1024
- symbol: symbol,
1025
- expiry: expiry,
1026
- found_instrument: found_instrument,
1027
- strikes_count: strikes_count
1028
- }
1029
- )
1030
- rescue StandardError => e
1031
- {
1032
- action: "get_option_chain",
1033
- error: e.message,
1034
- params: {
1035
- security_id: security_id,
1036
- symbol: symbol,
1037
- exchange_segment: exchange_segment,
1038
- expiry: expiry
1039
- }.compact
1040
- }
1041
- end
1042
-
1043
- def find_underlying_instrument(exchange_segment, instrument_symbol)
1044
- found_instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
1045
- return [exchange_segment, found_instrument] if found_instrument
1046
- return [exchange_segment, nil] unless %w[NSE_FNO BSE_FNO].include?(exchange_segment)
1047
-
1048
- fallback_instrument = DhanHQ::Models::Instrument.find("IDX_I", instrument_symbol)
1049
- return ["IDX_I", fallback_instrument] if fallback_instrument
1050
-
1051
- [exchange_segment, nil]
1052
- end
1053
-
1054
- def resolve_security_id_for_option_chain(security_id, found_instrument)
1055
- return security_id.to_i if security_id
1056
-
1057
- safe_instrument_attr(found_instrument, :security_id)&.to_i
1058
- end
1059
-
1060
- def option_chain_for_expiry(params)
1061
- resolved_security_id = params.fetch(:resolved_security_id)
1062
- underlying_seg = params.fetch(:underlying_seg)
1063
- exchange_segment = params.fetch(:exchange_segment)
1064
- symbol = params.fetch(:symbol)
1065
- expiry = params.fetch(:expiry)
1066
- found_instrument = params[:found_instrument]
1067
- strikes_count = params.fetch(:strikes_count, 5) # Default: 5 strikes (2 ITM, ATM, 2 OTM)
1068
-
1069
- # Ensure expiry is a string in YYYY-MM-DD format
1070
- expiry_str = expiry.to_s.strip
1071
-
1072
- # Validate expiry format (basic check)
1073
- unless expiry_str.match?(/^\d{4}-\d{2}-\d{2}$/)
1074
- return option_chain_error(
1075
- "expiry must be in YYYY-MM-DD format, got: #{expiry.inspect}",
1076
- security_id: resolved_security_id,
1077
- symbol: symbol,
1078
- exchange_segment: exchange_segment,
1079
- expiry: expiry
1080
- )
1081
- end
1082
-
1083
- # Ensure underlying_scrip is an integer (as per API docs)
1084
- underlying_scrip_int = resolved_security_id.to_i
1085
- unless underlying_scrip_int.positive?
1086
- return option_chain_error(
1087
- "underlying_scrip (security_id) must be a positive integer, got: #{resolved_security_id.inspect}",
1088
- security_id: resolved_security_id,
1089
- symbol: symbol,
1090
- exchange_segment: exchange_segment,
1091
- expiry: expiry
1092
- )
1093
- end
1094
-
1095
- # Validate underlying_seg (as per API docs: IDX_I, NSE_FNO, BSE_FNO, MCX_FO)
1096
- valid_underlying_segs = %w[IDX_I NSE_FNO BSE_FNO MCX_FO]
1097
- unless valid_underlying_segs.include?(underlying_seg)
1098
- return option_chain_error(
1099
- "underlying_seg must be one of: #{valid_underlying_segs.join(', ')}, got: #{underlying_seg.inspect}",
1100
- security_id: resolved_security_id,
1101
- symbol: symbol,
1102
- exchange_segment: exchange_segment,
1103
- expiry: expiry
1104
- )
1105
- end
1106
-
1107
- chain = DhanHQ::Models::OptionChain.fetch(
1108
- underlying_scrip: underlying_scrip_int,
1109
- underlying_seg: underlying_seg,
1110
- expiry: expiry_str
1111
- )
1112
-
1113
- # Extract last_price and filter chain to only relevant strikes around ATM
1114
- last_price = chain[:last_price] || chain["last_price"]
1115
- full_chain = chain[:oc] || chain["oc"] || {}
1116
-
1117
- # Filter chain to include strikes around ATM (default: 5 strikes = 2 ITM, ATM, 2 OTM)
1118
- filtered_chain = filter_option_chain(full_chain, last_price, strikes_count)
1119
-
1120
- {
1121
- action: "get_option_chain",
1122
- params: option_chain_params(
1123
- resolved_security_id: resolved_security_id,
1124
- exchange_segment: exchange_segment,
1125
- symbol: symbol,
1126
- expiry: expiry
1127
- ),
1128
- result: {
1129
- expiry: expiry,
1130
- underlying_last_price: last_price,
1131
- chain: filtered_chain,
1132
- chain_count: filtered_chain.is_a?(Hash) ? filtered_chain.keys.length : 0,
1133
- note: "Chain filtered to show #{strikes_count} strikes around ATM (ITM, ATM, OTM)",
1134
- instrument_info: option_chain_instrument_info(
1135
- resolved_security_id: resolved_security_id,
1136
- underlying_seg: underlying_seg,
1137
- found_instrument: found_instrument,
1138
- symbol: symbol
1139
- )
1140
- }
1141
- }
1142
- end
1143
-
1144
- def option_chain_expiry_list(resolved_security_id:, underlying_seg:, exchange_segment:, symbol:, found_instrument:)
1145
- expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
1146
- underlying_scrip: resolved_security_id,
1147
- underlying_seg: underlying_seg
1148
- )
1149
-
1150
- {
1151
- action: "get_option_chain",
1152
- params: option_chain_params(
1153
- resolved_security_id: resolved_security_id,
1154
- exchange_segment: exchange_segment,
1155
- symbol: symbol
1156
- ),
1157
- result: {
1158
- expiries: expiries,
1159
- count: expiries.is_a?(Array) ? expiries.length : 0,
1160
- instrument_info: option_chain_instrument_info(
1161
- resolved_security_id: resolved_security_id,
1162
- underlying_seg: underlying_seg,
1163
- found_instrument: found_instrument,
1164
- symbol: symbol
1165
- )
1166
- }
1167
- }
1168
- end
1169
-
1170
- def filter_option_chain(full_chain, last_price, strikes_count = 5)
1171
- return {} unless valid_option_chain_inputs?(full_chain, last_price)
1172
-
1173
- strikes = extract_chain_strikes(full_chain)
1174
- return {} if strikes.empty?
1175
-
1176
- selected_strikes = select_strike_keys(strikes, last_price.to_f, strikes_count)
1177
- return {} if selected_strikes.empty?
1178
-
1179
- build_filtered_chain(full_chain, selected_strikes)
1180
- end
1181
-
1182
- def option_chain_instrument_info(resolved_security_id:, underlying_seg:, found_instrument:, symbol:)
1183
- {
1184
- underlying_security_id: resolved_security_id,
1185
- underlying_seg: underlying_seg,
1186
- trading_symbol: found_instrument ? safe_instrument_attr(found_instrument, :trading_symbol) : symbol
1187
- }
1188
- end
1189
-
1190
- def option_chain_params(resolved_security_id:, exchange_segment:, symbol:, expiry: nil)
1191
- {
1192
- security_id: resolved_security_id,
1193
- symbol: symbol,
1194
- exchange_segment: exchange_segment,
1195
- expiry: expiry
1196
- }.compact
1197
- end
1198
-
1199
- def option_chain_error(message, security_id:, symbol:, exchange_segment:, expiry: nil)
1200
- {
1201
- action: "get_option_chain",
1202
- error: message,
1203
- params: {
1204
- security_id: security_id,
1205
- symbol: symbol,
1206
- exchange_segment: exchange_segment,
1207
- expiry: expiry
1208
- }.compact
1209
- }
1210
- end
1211
-
1212
- # 5. Expired Options Data API - Get historical expired options data using ExpiredOptionsData.fetch
1213
- #
1214
- # REQUIRED PARAMETERS:
1215
- # - exchange_segment [String]: Exchange and segment identifier
1216
- # Valid values: NSE_FNO, BSE_FNO, NSE_EQ, BSE_EQ
1217
- # - expiry_date [String]: Expiry date in YYYY-MM-DD format
1218
- # - symbol [String] OR security_id [Integer]: Must provide one
1219
- # symbol: Trading symbol of underlying
1220
- # security_id: Numeric security ID of underlying
1221
- #
1222
- # OPTIONAL PARAMETERS (with defaults):
1223
- # - interval [String]: Minute intervals for timeframe (default: "1")
1224
- # Valid values: "1", "5", "15", "25", "60"
1225
- # - instrument [String]: Instrument type (default: auto-detected)
1226
- # Valid values: "OPTIDX" (Index Options), "OPTSTK" (Stock Options)
1227
- # Default: "OPTIDX" for IDX_I, "OPTSTK" for others
1228
- # - expiry_flag [String]: Expiry interval (default: "MONTH")
1229
- # Valid values: "WEEK", "MONTH"
1230
- # - expiry_code [Integer]: Expiry code (default: 1 - near month)
1231
- # Valid values: 0 (far month), 1 (near month), 2 (current month)
1232
- # - strike [String]: Strike price specification (default: "ATM")
1233
- # Format: "ATM" for At The Money, "ATM+X" or "ATM-X" for offset strikes
1234
- # For Index Options (near expiry): Up to ATM+10 / ATM-10
1235
- # For all other contracts: Up to ATM+3 / ATM-3
1236
- # - drv_option_type [String]: Option type (default: "CALL")
1237
- # Valid values: "CALL", "PUT"
1238
- # - required_data [Array<String>]: Array of required data fields (default: all fields)
1239
- # Valid values: "open", "high", "low", "close", "iv", "volume", "strike", "oi", "spot"
1240
- #
1241
- # Returns: Historical expired options data organized by strike price relative to spot
1242
- #
1243
- # Notes:
1244
- # - Up to 31 days of data can be fetched in a single request
1245
- # - Historical data available for up to the last 5 years
1246
- # - Data is organized by strike price relative to spot
1247
- # - from_date is calculated from expiry_date, to_date is expiry_date + 1 day
1248
- #
1249
- # rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1250
- def get_expired_options_data(exchange_segment:, expiry_date:, security_id: nil, symbol: nil, expiry_code: nil,
1251
- interval: "1", instrument: nil, expiry_flag: "MONTH", strike: "ATM",
1252
- drv_option_type: "CALL", required_data: nil)
1253
- # CRITICAL: security_id must be an integer, not a symbol string
1254
- unless symbol || security_id
1255
- return {
1256
- action: "get_expired_options_data",
1257
- error: "Either symbol or security_id must be provided",
1258
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1259
- expiry_date: expiry_date }
1260
- }
1261
- end
1262
-
1263
- exchange_segment = exchange_segment.to_s
1264
-
1265
- # If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
1266
- if security_id && !symbol
1267
- is_valid, result = validate_security_id_numeric(security_id)
1268
- unless is_valid
1269
- return {
1270
- action: "get_expired_options_data",
1271
- error: result,
1272
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1273
- expiry_date: expiry_date }
1274
- }
1275
- end
1276
- security_id_int = result
1277
- unless security_id_int.positive?
1278
- return {
1279
- action: "get_expired_options_data",
1280
- error: "security_id must be a positive integer, got: #{security_id.inspect}",
1281
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1282
- expiry_date: expiry_date }
1283
- }
1284
- end
1285
- resolved_security_id = security_id_int
1286
- found_instrument = nil
1287
- else
1288
- # Find instrument using symbol (Instrument.find expects symbol, not security_id)
1289
- instrument_symbol = symbol.to_s
1290
- # Try original exchange_segment first, then try IDX_I for indices
1291
- found_instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
1292
- # If not found and exchange_segment is NSE_FNO/BSE_FNO, try IDX_I for index options
1293
- if !found_instrument && %w[NSE_FNO BSE_FNO].include?(exchange_segment)
1294
- found_instrument = DhanHQ::Models::Instrument.find("IDX_I", instrument_symbol)
1295
- end
1296
-
1297
- unless found_instrument
1298
- return {
1299
- action: "get_expired_options_data",
1300
- error: "Instrument not found. For options, you may need to provide security_id directly.",
1301
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1302
- expiry_date: expiry_date }
1303
- }
1304
- end
1305
-
1306
- # Get security_id from instrument
1307
- resolved_security_id = safe_instrument_attr(found_instrument, :security_id)
1308
- unless resolved_security_id
1309
- return {
1310
- action: "get_expired_options_data",
1311
- error: "security_id is required and could not be determined from instrument",
1312
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1313
- expiry_date: expiry_date }
1314
- }
1315
- end
1316
- resolved_security_id = resolved_security_id.to_i
1317
- end
1318
-
1319
- # Determine instrument type - must be OPTIDX or OPTSTK
1320
- resolved_instrument = instrument || safe_instrument_attr(found_instrument, :instrument_type)
1321
- resolved_instrument = resolved_instrument.to_s.upcase if resolved_instrument
1322
-
1323
- # Validate and set default instrument type
1324
- unless %w[OPTIDX OPTSTK].include?(resolved_instrument)
1325
- # Default to OPTIDX for index options (IDX_I), OPTSTK for others
1326
- resolved_instrument = if exchange_segment == "IDX_I"
1327
- "OPTIDX"
1328
- else
1329
- "OPTSTK"
1330
- end
1331
- end
1332
-
1333
- # Set default required_data if not provided
1334
- resolved_required_data = required_data || %w[open high low close volume iv oi strike spot]
1335
-
1336
- # expiry_code is required - use provided value or default to 1 (near month)
1337
- # Valid values: 0 (far month), 1 (near month), 2 (current month)
1338
- # Must be explicitly set, cannot be nil
1339
- resolved_expiry_code = if expiry_code.nil?
1340
- 1 # Default to near month
1341
- else
1342
- expiry_code.to_i
1343
- end
1344
-
1345
- # Ensure expiry_code is within valid range
1346
- unless [0, 1, 2].include?(resolved_expiry_code)
1347
- resolved_expiry_code = 1 # Fallback to near month if invalid
1348
- end
1349
-
1350
- # Calculate to_date (expiry_date + 1 day, or same day if single day range)
1351
- from_date_str = expiry_date.to_s
1352
- to_date_obj = Date.parse(from_date_str) + 1
1353
- to_date_str = to_date_obj.strftime("%Y-%m-%d")
1354
-
1355
- # Call ExpiredOptionsData.fetch with all required parameters
1356
- expired_data = DhanHQ::Models::ExpiredOptionsData.fetch(
1357
- exchange_segment: exchange_segment,
1358
- interval: interval.to_s,
1359
- security_id: resolved_security_id,
1360
- instrument: resolved_instrument,
1361
- expiry_flag: expiry_flag.to_s.upcase,
1362
- expiry_code: resolved_expiry_code.to_i,
1363
- strike: strike.to_s.upcase,
1364
- drv_option_type: drv_option_type.to_s.upcase,
1365
- required_data: resolved_required_data,
1366
- from_date: from_date_str,
1367
- to_date: to_date_str
1368
- )
1369
-
1370
- {
1371
- action: "get_expired_options_data",
1372
- params: { security_id: resolved_security_id, symbol: symbol, exchange_segment: exchange_segment,
1373
- expiry_date: expiry_date, expiry_code: resolved_expiry_code, interval: interval,
1374
- instrument: resolved_instrument, expiry_flag: expiry_flag, strike: strike,
1375
- drv_option_type: drv_option_type },
1376
- result: {
1377
- security_id: resolved_security_id,
1378
- exchange_segment: exchange_segment,
1379
- expiry_date: expiry_date,
1380
- data: expired_data.data,
1381
- call_data: expired_data.call_data,
1382
- put_data: expired_data.put_data,
1383
- ohlc_data: expired_data.ohlc_data,
1384
- volume_data: expired_data.volume_data,
1385
- summary_stats: expired_data.summary_stats,
1386
- instrument_info: {
1387
- trading_symbol: found_instrument ? safe_instrument_attr(found_instrument, :trading_symbol) : symbol,
1388
- instrument_type: resolved_instrument
1389
- }
1390
- }
1391
- }
1392
- rescue StandardError => e
1393
- {
1394
- action: "get_expired_options_data",
1395
- error: e.message,
1396
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
1397
- expiry_date: expiry_date }
1398
- }
1399
- end
1400
- # rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1401
-
1402
- private
1403
-
1404
- def action_error(action:, message:, exchange_segment:, security_id:, symbol:)
1405
- {
1406
- action: action,
1407
- error: message,
1408
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
1409
- }
1410
- end
1411
-
1412
- def require_symbol_or_security_id(action:, exchange_segment:, security_id:, symbol:)
1413
- return nil if symbol || security_id
1414
-
1415
- action_error(action: action,
1416
- message: "Either symbol or security_id must be provided",
1417
- exchange_segment: exchange_segment,
1418
- security_id: security_id,
1419
- symbol: symbol)
1420
- end
1421
-
1422
- def validated_security_id_for_marketfeed(action:, exchange_segment:, security_id:, symbol:)
1423
- is_valid, result = validate_security_id_numeric(security_id)
1424
- unless is_valid
1425
- return [action_error(action: action,
1426
- message: result,
1427
- exchange_segment: exchange_segment,
1428
- security_id: security_id,
1429
- symbol: symbol), nil]
1430
- end
1431
-
1432
- return [nil, result] if result.positive?
1433
-
1434
- [action_error(action: action,
1435
- message: "security_id must be a positive integer, got: #{security_id.inspect}",
1436
- exchange_segment: exchange_segment,
1437
- security_id: security_id,
1438
- symbol: symbol), nil]
1439
- end
1440
-
1441
- def extract_quote_data(quote_response, exchange_segment:, security_id:)
1442
- return unless quote_response.is_a?(Hash) && quote_response["data"]
1443
-
1444
- quote_response.dig("data", exchange_segment, security_id.to_s)
1445
- end
1446
-
1447
- def market_quote_response(exchange_segment:, security_id:, symbol:, quote:, instrument_symbol: nil)
1448
- result = {
1449
- security_id: security_id,
1450
- exchange_segment: exchange_segment,
1451
- quote: quote
1452
- }
1453
- result[:symbol] = instrument_symbol if instrument_symbol
1454
-
1455
- {
1456
- action: "get_market_quote",
1457
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
1458
- result: result
1459
- }
1460
- end
1461
-
1462
- def market_quote_from_security_id(exchange_segment:, security_id_int:, symbol:)
1463
- payload = { exchange_segment => [security_id_int] }
1464
- quote_response = DhanHQ::Models::MarketFeed.quote(payload)
1465
- quote_data = extract_quote_data(quote_response, exchange_segment: exchange_segment, security_id: security_id_int)
1466
-
1467
- market_quote_response(exchange_segment: exchange_segment,
1468
- security_id: security_id_int,
1469
- symbol: symbol,
1470
- quote: quote_data || quote_response)
1471
- end
1472
-
1473
- def market_quote_from_symbol(exchange_segment:, security_id:, symbol:)
1474
- instrument_symbol = symbol.to_s
1475
- instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
1476
- unless instrument
1477
- return action_error(action: "get_market_quote",
1478
- message: "Instrument not found",
1479
- exchange_segment: exchange_segment,
1480
- security_id: security_id,
1481
- symbol: symbol)
1482
- end
1483
-
1484
- quote_response = instrument.quote
1485
- resolved_security_id = safe_instrument_attr(instrument, :security_id) || security_id
1486
- quote_data = extract_quote_data(quote_response,
1487
- exchange_segment: exchange_segment,
1488
- security_id: resolved_security_id || security_id)
1489
-
1490
- market_quote_response(exchange_segment: exchange_segment,
1491
- security_id: resolved_security_id,
1492
- symbol: symbol,
1493
- quote: quote_data || quote_response,
1494
- instrument_symbol: instrument_symbol)
1495
- end
1496
-
1497
- def live_ltp_response(exchange_segment:, security_id:, symbol:, ltp_payload:, instrument_symbol: nil)
1498
- result = {
1499
- security_id: security_id,
1500
- exchange_segment: exchange_segment,
1501
- ltp: ltp_payload[:ltp],
1502
- ltp_data: ltp_payload[:ltp_data]
1503
- }
1504
- result[:symbol] = instrument_symbol if instrument_symbol
1505
-
1506
- {
1507
- action: "get_live_ltp",
1508
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
1509
- result: result
1510
- }
1511
- end
1512
-
1513
- def ltp_from_marketfeed(exchange_segment:, security_id:)
1514
- payload = { exchange_segment => [security_id] }
1515
- ltp_response = DhanHQ::Models::MarketFeed.ltp(payload)
1516
- parse_ltp_response(ltp_response, exchange_segment: exchange_segment, security_id: security_id)
1517
- end
1518
-
1519
- def ltp_from_instrument(exchange_segment:, instrument:, security_id:)
1520
- ltp_response = instrument.ltp
1521
- resolved_security_id = safe_instrument_attr(instrument, :security_id) || security_id
1522
- ltp, ltp_data = parse_ltp_response(ltp_response,
1523
- exchange_segment: exchange_segment,
1524
- security_id: resolved_security_id || security_id)
1525
-
1526
- [ltp, ltp_data, resolved_security_id]
1527
- end
1528
-
1529
- def parse_ltp_response(ltp_response, exchange_segment:, security_id:)
1530
- if ltp_response.is_a?(Hash) && ltp_response["data"]
1531
- security_id_str = security_id.to_s
1532
- ltp_data = ltp_response.dig("data", exchange_segment, security_id_str)
1533
- ltp = extract_value(ltp_data, [:last_price, "last_price"]) if ltp_data
1534
- return [ltp, ltp_data]
1535
- end
1536
-
1537
- if ltp_response.is_a?(Numeric)
1538
- ltp = ltp_response
1539
- return [ltp, { last_price: ltp }]
1540
- end
1541
-
1542
- ltp = extract_value(ltp_response, [:last_price, "last_price", :ltp, "ltp"]) || ltp_response
1543
- [ltp, ltp_response]
1544
- end
1545
-
1546
- def valid_option_chain_inputs?(full_chain, last_price)
1547
- return false unless full_chain.is_a?(Hash)
1548
- return false if last_price.nil?
1549
-
1550
- last_price.to_f.nonzero?
1551
- end
1552
-
1553
- def extract_chain_strikes(full_chain)
1554
- strikes = full_chain.filter_map do |strike_key, strike_data|
1555
- next unless strike_data.is_a?(Hash)
1556
- next unless strike_has_both_sides?(strike_data)
1557
-
1558
- strike_float = strike_key.to_f
1559
- next unless strike_float.positive?
1560
-
1561
- [strike_key, strike_float]
1562
- end
1563
- strikes.sort_by { |(_, price)| price }
1564
- end
1565
-
1566
- def strike_has_both_sides?(strike_data)
1567
- (strike_data.key?(:ce) || strike_data.key?("ce")) &&
1568
- (strike_data.key?(:pe) || strike_data.key?("pe"))
1569
- end
1570
-
1571
- def select_strike_keys(strikes, last_price, strikes_count)
1572
- atm_strike = strikes.min_by { |_, price| (price - last_price).abs }
1573
- return [] unless atm_strike
1574
-
1575
- atm_index = strikes.index(atm_strike)
1576
- return [] unless atm_index
1577
-
1578
- total_strikes = [strikes_count.to_i, 1].max
1579
- itm_count = (total_strikes - 1) / 2
1580
- otm_count = total_strikes - 1 - itm_count
1581
-
1582
- selected = []
1583
- selected.concat(collect_strike_keys(strikes, atm_index - itm_count, atm_index - 1))
1584
- selected << atm_strike[0]
1585
- selected.concat(collect_strike_keys(strikes, atm_index + 1, atm_index + otm_count))
1586
- selected
1587
- end
1588
-
1589
- def collect_strike_keys(strikes, start_idx, end_idx)
1590
- return [] if start_idx > end_idx
1591
-
1592
- bounded_start = [start_idx, 0].max
1593
- bounded_end = [end_idx, strikes.length - 1].min
1594
- return [] if bounded_start > bounded_end
1595
-
1596
- strikes[bounded_start..bounded_end].map { |strike| strike[0] }
1597
- end
1598
-
1599
- def build_filtered_chain(full_chain, selected_strikes)
1600
- selected_strikes.each_with_object({}) do |strike_key, filtered|
1601
- filtered[strike_key] = full_chain[strike_key] if full_chain.key?(strike_key)
1602
- end
1603
- end
1604
- end
1605
- end
1606
-
1607
- # DhanHQ Trading Tools - Order parameter building only
1608
- class DhanHQTradingTools
1609
- class << self
1610
- # Build order parameters (does not place order)
1611
- def build_order_params(params)
1612
- {
1613
- action: "place_order",
1614
- params: params,
1615
- order_params: {
1616
- transaction_type: params[:transaction_type] || "BUY",
1617
- exchange_segment: params[:exchange_segment] || "NSE_EQ",
1618
- product_type: params[:product_type] || "MARGIN",
1619
- order_type: params[:order_type] || "LIMIT",
1620
- validity: params[:validity] || "DAY",
1621
- security_id: params[:security_id],
1622
- quantity: params[:quantity] || 1,
1623
- price: params[:price]
1624
- },
1625
- message: "Order parameters ready: #{params[:transaction_type]} " \
1626
- "#{params[:quantity]} #{params[:security_id]} @ #{params[:price]}"
1627
- }
1628
- end
1629
-
1630
- # Build super order parameters (does not place order)
1631
- def build_super_order_params(params)
1632
- {
1633
- action: "place_super_order",
1634
- params: params,
1635
- order_params: {
1636
- transaction_type: params[:transaction_type] || "BUY",
1637
- exchange_segment: params[:exchange_segment] || "NSE_EQ",
1638
- product_type: params[:product_type] || "MARGIN",
1639
- order_type: params[:order_type] || "LIMIT",
1640
- security_id: params[:security_id],
1641
- quantity: params[:quantity] || 1,
1642
- price: params[:price],
1643
- target_price: params[:target_price],
1644
- stop_loss_price: params[:stop_loss_price],
1645
- trailing_jump: params[:trailing_jump] || 10
1646
- },
1647
- message: "Super order parameters ready: Entry @ #{params[:price]}, " \
1648
- "SL: #{params[:stop_loss_price]}, TP: #{params[:target_price]}"
1649
- }
1650
- end
1651
-
1652
- # Build cancel order parameters (does not cancel)
1653
- def build_cancel_params(order_id:)
1654
- {
1655
- action: "cancel_order",
1656
- params: { order_id: order_id },
1657
- message: "Cancel parameters ready for order: #{order_id}",
1658
- note: "To actually cancel, call: DhanHQ::Models::Order.find(order_id).cancel"
1659
- }
1660
- end
1661
- end
1662
- end
1663
-