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.
@@ -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
- return [false,
77
- "security_id must be numeric (integer or numeric string like '13'), not a symbol string like '#{cleaned}'. Use symbol parameter for symbols."]
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
- # CRITICAL: security_id must be an integer, not a symbol string
245
- # If security_id is provided, we cannot use Instrument.find (which expects symbol)
246
- # We need symbol to find the instrument, or we need to use security_id directly with MarketFeed API
247
- unless symbol || security_id
248
- return {
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 # Enforce rate limiting
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
- is_valid, result = validate_security_id_numeric(security_id)
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
- params: { security_id: security_id_int, symbol: symbol, exchange_segment: exchange_segment },
289
- result: {
290
- security_id: security_id_int,
291
- exchange_segment: exchange_segment,
292
- quote: quote_data || quote_response
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
- action: "get_market_quote",
315
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
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
- action: "get_market_quote",
333
- error: e.message,
334
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
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
- # CRITICAL: security_id must be an integer, not a symbol string
355
- unless symbol || security_id
356
- return {
357
- action: "get_live_ltp",
358
- error: "Either symbol or security_id must be provided",
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 # Enforce rate limiting
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
- is_valid, result = validate_security_id_numeric(security_id)
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
- params: { security_id: security_id_int, symbol: symbol, exchange_segment: exchange_segment },
400
- result: {
401
- security_id: security_id_int,
402
- exchange_segment: exchange_segment,
403
- ltp: ltp,
404
- ltp_data: ltp_data
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
- if instrument
414
- # Use instrument convenience method - automatically uses instrument's attributes
415
- # Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{"last_price"=>1578.1}}}, "status"=>"success"}
416
- # OR direct value: 1578.1 (after retry/rate limit handling)
417
- ltp_response = instrument.ltp
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
- action: "get_live_ltp",
453
- error: e.message,
454
- params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
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: { security_id: resolved_security_id, symbol: symbol, exchange_segment: exchange_segment,
729
- instrument: resolved_instrument, from_date: from_date, to_date: to_date, interval: interval },
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. Raw data not included to reduce response size.",
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: { security_id: resolved_security_id, symbol: symbol, exchange_segment: exchange_segment,
746
- instrument: resolved_instrument, from_date: from_date, to_date: to_date, interval: interval },
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: { security_id: resolved_security_id, symbol: symbol, exchange_segment: exchange_segment,
785
- instrument: resolved_instrument, from_date: from_date, to_date: to_date, expiry_code: expiry_code },
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. Raw data not included to reduce response size.",
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: { security_id: resolved_security_id, symbol: symbol, exchange_segment: exchange_segment,
802
- instrument: resolved_instrument, from_date: from_date, to_date: to_date, expiry_code: expiry_code },
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 full_chain.is_a?(Hash) && last_price
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
- # Sort strikes by price
1277
- strikes.sort_by! { |_, price| price }
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
- # Get OTM strikes (above ATM)
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