ollama-client 0.2.2 → 0.2.3
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 +8 -0
- data/README.md +7 -1
- data/docs/CLOUD.md +29 -0
- data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
- data/docs/README.md +37 -0
- data/docs/SCHEMA_FIXES.md +147 -0
- data/docs/TEST_UPDATES.md +107 -0
- data/examples/README.md +92 -0
- data/examples/advanced_complex_schemas.rb +6 -3
- data/examples/advanced_multi_step_agent.rb +2 -1
- data/examples/chat_console.rb +12 -3
- data/examples/complete_workflow.rb +14 -4
- data/examples/dhan_console.rb +103 -8
- data/examples/dhanhq/agents/technical_analysis_agent.rb +6 -1
- data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
- data/examples/dhanhq_agent.rb +23 -13
- data/examples/dhanhq_tools.rb +311 -246
- data/examples/multi_step_agent_with_external_data.rb +368 -0
- data/{test_dhanhq_tool_calling.rb → examples/test_dhanhq_tool_calling.rb} +99 -6
- data/lib/ollama/agent/executor.rb +30 -30
- data/lib/ollama/client.rb +73 -80
- data/lib/ollama/dto.rb +7 -7
- data/lib/ollama/options.rb +17 -9
- data/lib/ollama/response.rb +4 -6
- data/lib/ollama/tool/function/parameters.rb +1 -0
- data/lib/ollama/version.rb +1 -1
- metadata +14 -7
- /data/{FEATURES_ADDED.md → docs/FEATURES_ADDED.md} +0 -0
- /data/{HANDLERS_ANALYSIS.md → docs/HANDLERS_ANALYSIS.md} +0 -0
- /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
- /data/{TESTING.md → docs/TESTING.md} +0 -0
- /data/{test_tool_calling.rb → examples/test_tool_calling.rb} +0 -0
data/examples/dhanhq_tools.rb
CHANGED
|
@@ -73,8 +73,9 @@ def validate_security_id_numeric(security_id)
|
|
|
73
73
|
return [true, cleaned.to_i] if cleaned.match?(/^\d+$/)
|
|
74
74
|
|
|
75
75
|
# It's a non-numeric string (likely a symbol like "NIFTY")
|
|
76
|
-
|
|
77
|
-
|
|
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]
|
|
78
79
|
|
|
79
80
|
end
|
|
80
81
|
|
|
@@ -241,98 +242,36 @@ class DhanHQDataTools
|
|
|
241
242
|
#
|
|
242
243
|
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
243
244
|
def get_market_quote(exchange_segment:, security_id: nil, symbol: nil)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
action: "get_market_quote",
|
|
250
|
-
error: "Either symbol or security_id must be provided",
|
|
251
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
252
|
-
}
|
|
253
|
-
end
|
|
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
|
|
254
250
|
|
|
255
|
-
rate_limit_marketfeed
|
|
251
|
+
rate_limit_marketfeed
|
|
256
252
|
exchange_segment = exchange_segment.to_s
|
|
257
253
|
|
|
258
|
-
# If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
|
|
259
|
-
# If symbol is provided, find instrument first to get security_id
|
|
260
254
|
if security_id && !symbol
|
|
261
|
-
|
|
262
|
-
unless is_valid
|
|
263
|
-
return {
|
|
264
|
-
action: "get_market_quote",
|
|
265
|
-
error: result,
|
|
266
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
267
|
-
}
|
|
268
|
-
end
|
|
269
|
-
security_id_int = result
|
|
270
|
-
unless security_id_int.positive?
|
|
271
|
-
return {
|
|
272
|
-
action: "get_market_quote",
|
|
273
|
-
error: "security_id must be a positive integer, got: #{security_id.inspect}",
|
|
274
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
275
|
-
}
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Use MarketFeed.quote directly with security_id
|
|
279
|
-
payload = { exchange_segment => [security_id_int] }
|
|
280
|
-
quote_response = DhanHQ::Models::MarketFeed.quote(payload)
|
|
281
|
-
|
|
282
|
-
if quote_response.is_a?(Hash) && quote_response["data"]
|
|
283
|
-
quote_data = quote_response.dig("data", exchange_segment, security_id_int.to_s)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
return {
|
|
255
|
+
error, security_id_int = validated_security_id_for_marketfeed(
|
|
287
256
|
action: "get_market_quote",
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Find instrument using symbol (Instrument.find expects symbol, not security_id)
|
|
298
|
-
instrument_symbol = symbol.to_s
|
|
299
|
-
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
300
|
-
|
|
301
|
-
if instrument
|
|
302
|
-
# Use instrument convenience method - automatically uses instrument's attributes
|
|
303
|
-
# Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
|
|
304
|
-
quote_response = instrument.quote
|
|
305
|
-
|
|
306
|
-
# Extract actual quote data from nested structure
|
|
307
|
-
security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
|
|
308
|
-
if quote_response.is_a?(Hash) && quote_response["data"]
|
|
309
|
-
quote_data = quote_response.dig("data", exchange_segment,
|
|
310
|
-
security_id_str)
|
|
311
|
-
end
|
|
257
|
+
exchange_segment: exchange_segment,
|
|
258
|
+
security_id: security_id,
|
|
259
|
+
symbol: symbol
|
|
260
|
+
)
|
|
261
|
+
return error if error
|
|
312
262
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
result: {
|
|
317
|
-
security_id: safe_instrument_attr(instrument, :security_id) || security_id,
|
|
318
|
-
symbol: instrument_symbol,
|
|
319
|
-
exchange_segment: exchange_segment,
|
|
320
|
-
quote: quote_data || quote_response
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else
|
|
324
|
-
{
|
|
325
|
-
action: "get_market_quote",
|
|
326
|
-
error: "Instrument not found",
|
|
327
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
328
|
-
}
|
|
263
|
+
return market_quote_from_security_id(exchange_segment: exchange_segment,
|
|
264
|
+
security_id_int: security_id_int,
|
|
265
|
+
symbol: symbol)
|
|
329
266
|
end
|
|
267
|
+
|
|
268
|
+
market_quote_from_symbol(exchange_segment: exchange_segment, security_id: security_id, symbol: symbol)
|
|
330
269
|
rescue StandardError => e
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
270
|
+
action_error(action: "get_market_quote",
|
|
271
|
+
message: e.message,
|
|
272
|
+
exchange_segment: exchange_segment,
|
|
273
|
+
security_id: security_id,
|
|
274
|
+
symbol: symbol)
|
|
336
275
|
end
|
|
337
276
|
|
|
338
277
|
# 2. Live Market Feed API - Get LTP (Last Traded Price) using Instrument convenience method
|
|
@@ -351,108 +290,55 @@ class DhanHQDataTools
|
|
|
351
290
|
#
|
|
352
291
|
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
353
292
|
def get_live_ltp(exchange_segment:, security_id: nil, symbol: nil)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
360
|
-
}
|
|
361
|
-
end
|
|
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
|
|
362
298
|
|
|
363
|
-
rate_limit_marketfeed
|
|
299
|
+
rate_limit_marketfeed
|
|
364
300
|
exchange_segment = exchange_segment.to_s
|
|
365
301
|
|
|
366
|
-
# If security_id is provided, use it directly - it must be numeric (integer or numeric string), not a symbol
|
|
367
302
|
if security_id && !symbol
|
|
368
|
-
|
|
369
|
-
unless is_valid
|
|
370
|
-
return {
|
|
371
|
-
action: "get_live_ltp",
|
|
372
|
-
error: result,
|
|
373
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
374
|
-
}
|
|
375
|
-
end
|
|
376
|
-
security_id_int = result
|
|
377
|
-
unless security_id_int.positive?
|
|
378
|
-
return {
|
|
379
|
-
action: "get_live_ltp",
|
|
380
|
-
error: "security_id must be a positive integer, got: #{security_id.inspect}",
|
|
381
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
382
|
-
}
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
# Use MarketFeed.ltp directly with security_id
|
|
386
|
-
payload = { exchange_segment => [security_id_int] }
|
|
387
|
-
ltp_response = DhanHQ::Models::MarketFeed.ltp(payload)
|
|
388
|
-
|
|
389
|
-
if ltp_response.is_a?(Hash) && ltp_response["data"]
|
|
390
|
-
ltp_data = ltp_response.dig("data", exchange_segment, security_id_int.to_s)
|
|
391
|
-
ltp = extract_value(ltp_data, [:last_price, "last_price"]) if ltp_data
|
|
392
|
-
else
|
|
393
|
-
ltp = extract_value(ltp_response, [:last_price, "last_price", :ltp, "ltp"]) || ltp_response
|
|
394
|
-
ltp_data = ltp_response
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
return {
|
|
303
|
+
error, security_id_int = validated_security_id_for_marketfeed(
|
|
398
304
|
action: "get_live_ltp",
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 })
|
|
407
316
|
end
|
|
408
317
|
|
|
409
|
-
# Find instrument using symbol (Instrument.find expects symbol, not security_id)
|
|
410
318
|
instrument_symbol = symbol.to_s
|
|
411
319
|
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
# Extract LTP from nested structure or use direct value
|
|
420
|
-
if ltp_response.is_a?(Hash) && ltp_response["data"]
|
|
421
|
-
security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
|
|
422
|
-
ltp_data = ltp_response.dig("data", exchange_segment, security_id_str)
|
|
423
|
-
ltp = extract_value(ltp_data, [:last_price, "last_price"]) if ltp_data
|
|
424
|
-
elsif ltp_response.is_a?(Numeric)
|
|
425
|
-
ltp = ltp_response
|
|
426
|
-
ltp_data = { last_price: ltp }
|
|
427
|
-
else
|
|
428
|
-
ltp = extract_value(ltp_response, [:last_price, "last_price", :ltp, "ltp"]) || ltp_response
|
|
429
|
-
ltp_data = ltp_response
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
{
|
|
433
|
-
action: "get_live_ltp",
|
|
434
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
|
|
435
|
-
result: {
|
|
436
|
-
security_id: safe_instrument_attr(instrument, :security_id) || security_id,
|
|
437
|
-
symbol: instrument_symbol,
|
|
438
|
-
exchange_segment: exchange_segment,
|
|
439
|
-
ltp: ltp,
|
|
440
|
-
ltp_data: ltp_data
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
else
|
|
444
|
-
{
|
|
445
|
-
action: "get_live_ltp",
|
|
446
|
-
error: "Instrument not found",
|
|
447
|
-
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
448
|
-
}
|
|
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)
|
|
449
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)
|
|
450
336
|
rescue StandardError => e
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
337
|
+
action_error(action: "get_live_ltp",
|
|
338
|
+
message: e.message,
|
|
339
|
+
exchange_segment: exchange_segment,
|
|
340
|
+
security_id: security_id,
|
|
341
|
+
symbol: symbol)
|
|
456
342
|
end
|
|
457
343
|
|
|
458
344
|
# 3. Full Market Depth API - Get full market depth (bid/ask levels)
|
|
@@ -725,12 +611,20 @@ class DhanHQDataTools
|
|
|
725
611
|
{
|
|
726
612
|
action: "get_historical_data",
|
|
727
613
|
type: "intraday",
|
|
728
|
-
params: {
|
|
729
|
-
|
|
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
|
+
},
|
|
730
623
|
result: {
|
|
731
624
|
indicators: indicators,
|
|
732
625
|
data_points: count,
|
|
733
|
-
note: "Technical indicators calculated from historical data.
|
|
626
|
+
note: "Technical indicators calculated from historical data. " \
|
|
627
|
+
"Raw data not included to reduce response size.",
|
|
734
628
|
instrument_info: {
|
|
735
629
|
security_id: resolved_security_id,
|
|
736
630
|
trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
|
|
@@ -742,8 +636,15 @@ class DhanHQDataTools
|
|
|
742
636
|
{
|
|
743
637
|
action: "get_historical_data",
|
|
744
638
|
type: "intraday",
|
|
745
|
-
params: {
|
|
746
|
-
|
|
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
|
+
},
|
|
747
648
|
result: {
|
|
748
649
|
data: data,
|
|
749
650
|
count: count,
|
|
@@ -781,12 +682,20 @@ class DhanHQDataTools
|
|
|
781
682
|
{
|
|
782
683
|
action: "get_historical_data",
|
|
783
684
|
type: "daily",
|
|
784
|
-
params: {
|
|
785
|
-
|
|
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
|
+
},
|
|
786
694
|
result: {
|
|
787
695
|
indicators: indicators,
|
|
788
696
|
data_points: count,
|
|
789
|
-
note: "Technical indicators calculated from historical data.
|
|
697
|
+
note: "Technical indicators calculated from historical data. " \
|
|
698
|
+
"Raw data not included to reduce response size.",
|
|
790
699
|
instrument_info: {
|
|
791
700
|
security_id: resolved_security_id,
|
|
792
701
|
trading_symbol: safe_instrument_attr(found_instrument, :trading_symbol),
|
|
@@ -798,8 +707,15 @@ class DhanHQDataTools
|
|
|
798
707
|
{
|
|
799
708
|
action: "get_historical_data",
|
|
800
709
|
type: "daily",
|
|
801
|
-
params: {
|
|
802
|
-
|
|
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
|
+
},
|
|
803
719
|
result: {
|
|
804
720
|
data: data,
|
|
805
721
|
count: count,
|
|
@@ -1252,69 +1168,15 @@ class DhanHQDataTools
|
|
|
1252
1168
|
end
|
|
1253
1169
|
|
|
1254
1170
|
def filter_option_chain(full_chain, last_price, strikes_count = 5)
|
|
1255
|
-
return {} unless
|
|
1256
|
-
|
|
1257
|
-
last_price_float = last_price.to_f
|
|
1258
|
-
return {} if last_price_float.zero?
|
|
1259
|
-
|
|
1260
|
-
# Extract all strike prices that have both CE and PE data
|
|
1261
|
-
strikes = full_chain.keys.map do |strike_key|
|
|
1262
|
-
strike_data = full_chain[strike_key]
|
|
1263
|
-
next unless strike_data.is_a?(Hash)
|
|
1264
|
-
|
|
1265
|
-
# Check if strike has both CE and PE
|
|
1266
|
-
has_ce = strike_data.key?(:ce) || strike_data.key?("ce")
|
|
1267
|
-
has_pe = strike_data.key?(:pe) || strike_data.key?("pe")
|
|
1268
|
-
next unless has_ce && has_pe
|
|
1269
|
-
|
|
1270
|
-
strike_float = strike_key.to_f
|
|
1271
|
-
[strike_key, strike_float] if strike_float.positive?
|
|
1272
|
-
end.compact
|
|
1171
|
+
return {} unless valid_option_chain_inputs?(full_chain, last_price)
|
|
1273
1172
|
|
|
1173
|
+
strikes = extract_chain_strikes(full_chain)
|
|
1274
1174
|
return {} if strikes.empty?
|
|
1275
1175
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
# Find ATM strike (closest to last_price)
|
|
1280
|
-
atm_strike = strikes.min_by { |_, price| (price - last_price_float).abs }
|
|
1281
|
-
return {} unless atm_strike
|
|
1282
|
-
|
|
1283
|
-
atm_index = strikes.index(atm_strike)
|
|
1284
|
-
return {} unless atm_index
|
|
1285
|
-
|
|
1286
|
-
# Calculate how many strikes to get on each side of ATM
|
|
1287
|
-
# Default: 5 strikes = 2 ITM, ATM, 2 OTM
|
|
1288
|
-
# For odd numbers: distribute evenly (e.g., 5 = 2 ITM + ATM + 2 OTM)
|
|
1289
|
-
# For even numbers: prefer OTM (e.g., 6 = 2 ITM + ATM + 3 OTM)
|
|
1290
|
-
total_strikes = [strikes_count.to_i, 1].max # At least 1 (ATM)
|
|
1291
|
-
itm_count = (total_strikes - 1) / 2 # Rounds down for odd, up for even
|
|
1292
|
-
otm_count = total_strikes - 1 - itm_count # Remaining after ATM
|
|
1293
|
-
|
|
1294
|
-
selected_strikes = []
|
|
1295
|
-
|
|
1296
|
-
# Get ITM strikes (below ATM)
|
|
1297
|
-
itm_start = [atm_index - itm_count, 0].max
|
|
1298
|
-
itm_start.upto(atm_index - 1) do |i|
|
|
1299
|
-
selected_strikes << strikes[i][0] if strikes[i]
|
|
1300
|
-
end
|
|
1301
|
-
|
|
1302
|
-
# ATM
|
|
1303
|
-
selected_strikes << atm_strike[0]
|
|
1176
|
+
selected_strikes = select_strike_keys(strikes, last_price.to_f, strikes_count)
|
|
1177
|
+
return {} if selected_strikes.empty?
|
|
1304
1178
|
|
|
1305
|
-
|
|
1306
|
-
otm_end = [atm_index + otm_count, strikes.length - 1].min
|
|
1307
|
-
(atm_index + 1).upto(otm_end) do |i|
|
|
1308
|
-
selected_strikes << strikes[i][0] if strikes[i]
|
|
1309
|
-
end
|
|
1310
|
-
|
|
1311
|
-
# Filter chain to only include selected strikes
|
|
1312
|
-
filtered = {}
|
|
1313
|
-
selected_strikes.each do |strike_key|
|
|
1314
|
-
filtered[strike_key] = full_chain[strike_key] if full_chain.key?(strike_key)
|
|
1315
|
-
end
|
|
1316
|
-
|
|
1317
|
-
filtered
|
|
1179
|
+
build_filtered_chain(full_chain, selected_strikes)
|
|
1318
1180
|
end
|
|
1319
1181
|
|
|
1320
1182
|
def option_chain_instrument_info(resolved_security_id:, underlying_seg:, found_instrument:, symbol:)
|
|
@@ -1536,6 +1398,209 @@ class DhanHQDataTools
|
|
|
1536
1398
|
}
|
|
1537
1399
|
end
|
|
1538
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
|
|
1539
1604
|
end
|
|
1540
1605
|
end
|
|
1541
1606
|
|