ollama-client 0.2.4 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +560 -106
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/README.md +2 -3
- data/docs/RELEASE_GUIDE.md +376 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/docs/ruby_guide.md +6232 -0
- data/examples/README.md +51 -66
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +43 -21
- data/lib/ollama/config.rb +4 -1
- data/lib/ollama/document_loader.rb +163 -0
- data/lib/ollama/embeddings.rb +42 -13
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +8 -0
- metadata +31 -53
- data/docs/GEM_RELEASE_GUIDE.md +0 -794
- data/docs/GET_RUBYGEMS_SECRET.md +0 -151
- data/docs/QUICK_OTP_SETUP.md +0 -80
- data/docs/QUICK_RELEASE.md +0 -106
- data/docs/RUBYGEMS_OTP_SETUP.md +0 -199
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- data/exe/dhan_console +0 -4
data/examples/dhanhq_tools.rb
DELETED
|
@@ -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
|
-
|