ollama-client 0.2.1 → 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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +220 -12
  4. data/docs/CLOUD.md +29 -0
  5. data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
  6. data/docs/FEATURES_ADDED.md +145 -0
  7. data/docs/HANDLERS_ANALYSIS.md +190 -0
  8. data/docs/README.md +37 -0
  9. data/docs/SCHEMA_FIXES.md +147 -0
  10. data/docs/TEST_UPDATES.md +107 -0
  11. data/examples/README.md +92 -0
  12. data/examples/advanced_complex_schemas.rb +6 -3
  13. data/examples/advanced_multi_step_agent.rb +13 -7
  14. data/examples/chat_console.rb +143 -0
  15. data/examples/complete_workflow.rb +14 -4
  16. data/examples/dhan_console.rb +843 -0
  17. data/examples/dhanhq/agents/base_agent.rb +0 -2
  18. data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
  19. data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
  20. data/examples/dhanhq/analysis/market_structure.rb +44 -28
  21. data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
  22. data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
  23. data/examples/dhanhq/dhanhq_agent.rb +296 -99
  24. data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
  25. data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
  26. data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
  27. data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
  28. data/examples/dhanhq/services/data_service.rb +5 -7
  29. data/examples/dhanhq/services/trading_service.rb +0 -3
  30. data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
  31. data/examples/dhanhq/technical_analysis_runner.rb +216 -162
  32. data/examples/dhanhq/test_tool_calling.rb +538 -0
  33. data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
  34. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
  35. data/examples/dhanhq_agent.rb +159 -116
  36. data/examples/dhanhq_tools.rb +1158 -251
  37. data/examples/multi_step_agent_with_external_data.rb +368 -0
  38. data/examples/structured_tools.rb +89 -0
  39. data/examples/test_dhanhq_tool_calling.rb +375 -0
  40. data/examples/test_tool_calling.rb +160 -0
  41. data/examples/tool_calling_direct.rb +124 -0
  42. data/examples/tool_dto_example.rb +94 -0
  43. data/exe/dhan_console +4 -0
  44. data/exe/ollama-client +1 -1
  45. data/lib/ollama/agent/executor.rb +116 -15
  46. data/lib/ollama/client.rb +118 -55
  47. data/lib/ollama/config.rb +36 -0
  48. data/lib/ollama/dto.rb +187 -0
  49. data/lib/ollama/embeddings.rb +77 -0
  50. data/lib/ollama/options.rb +104 -0
  51. data/lib/ollama/response.rb +121 -0
  52. data/lib/ollama/tool/function/parameters/property.rb +72 -0
  53. data/lib/ollama/tool/function/parameters.rb +101 -0
  54. data/lib/ollama/tool/function.rb +78 -0
  55. data/lib/ollama/tool.rb +60 -0
  56. data/lib/ollama/version.rb +1 -1
  57. data/lib/ollama_client.rb +3 -0
  58. metadata +31 -3
  59. /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
  60. /data/{TESTING.md → docs/TESTING.md} +0 -0
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # DhanHQ Tool Calling Test - Verbose Version
5
+ # Shows the complete tool calling flow: LLM decides → Executor executes
6
+
7
+ require_relative "../../lib/ollama_client"
8
+ require_relative "../dhanhq_tools"
9
+
10
+ puts "\n=== DHANHQ TOOL CALLING TEST (VERBOSE) ===\n"
11
+ puts "This demonstrates REAL tool calling:\n"
12
+ puts " 1. LLM receives user query + tool definitions"
13
+ puts " 2. LLM DECIDES which tools to call (not your code!)"
14
+ puts " 3. LLM returns tool_calls in response"
15
+ puts " 4. Executor detects tool_calls and executes callables"
16
+ puts " 5. Tool results fed back to LLM"
17
+ puts " 6. LLM generates final answer\n"
18
+
19
+ # Configure DhanHQ
20
+ begin
21
+ DhanHQ.configure_with_env
22
+ puts "✅ DhanHQ configured\n"
23
+ rescue StandardError => e
24
+ puts "⚠️ DhanHQ configuration error: #{e.message}\n"
25
+ end
26
+
27
+ # Create client
28
+ config = Ollama::Config.new
29
+ config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
30
+ config.temperature = 0.2
31
+ config.timeout = 60
32
+ client = Ollama::Client.new(config: config)
33
+
34
+ # Define tools
35
+ market_quote_tool = Ollama::Tool.new(
36
+ type: "function",
37
+ function: Ollama::Tool::Function.new(
38
+ name: "get_market_quote",
39
+ description: "Get market quote for a symbol",
40
+ parameters: Ollama::Tool::Function::Parameters.new(
41
+ type: "object",
42
+ properties: {
43
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
44
+ type: "string",
45
+ description: "Stock symbol"
46
+ ),
47
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
48
+ type: "string",
49
+ description: "Exchange segment",
50
+ enum: %w[NSE_EQ BSE_EQ IDX_I]
51
+ )
52
+ },
53
+ required: %w[symbol exchange_segment]
54
+ )
55
+ )
56
+ )
57
+
58
+ live_ltp_tool = Ollama::Tool.new(
59
+ type: "function",
60
+ function: Ollama::Tool::Function.new(
61
+ name: "get_live_ltp",
62
+ description: "Get live last traded price",
63
+ parameters: Ollama::Tool::Function::Parameters.new(
64
+ type: "object",
65
+ properties: {
66
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
67
+ type: "string",
68
+ description: "Stock symbol"
69
+ ),
70
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
71
+ type: "string",
72
+ description: "Exchange segment",
73
+ enum: %w[NSE_EQ BSE_EQ IDX_I]
74
+ )
75
+ },
76
+ required: %w[symbol exchange_segment]
77
+ )
78
+ )
79
+ )
80
+
81
+ option_chain_tool = Ollama::Tool.new(
82
+ type: "function",
83
+ function: Ollama::Tool::Function.new(
84
+ name: "get_option_chain",
85
+ description: "Get option chain for an index (NIFTY, SENSEX, BANKNIFTY). " \
86
+ "Returns available expiries and option chain data with strikes, Greeks, OI, and IV.",
87
+ parameters: Ollama::Tool::Function::Parameters.new(
88
+ type: "object",
89
+ properties: {
90
+ symbol: Ollama::Tool::Function::Parameters::Property.new(
91
+ type: "string",
92
+ description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY)",
93
+ enum: %w[NIFTY SENSEX BANKNIFTY]
94
+ ),
95
+ exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
96
+ type: "string",
97
+ description: "Exchange segment (must be IDX_I for indices)",
98
+ enum: %w[IDX_I]
99
+ ),
100
+ expiry: Ollama::Tool::Function::Parameters::Property.new(
101
+ type: "string",
102
+ description: "Optional expiry date (YYYY-MM-DD format). If not provided, returns available expiries list."
103
+ )
104
+ },
105
+ required: %w[symbol exchange_segment]
106
+ )
107
+ )
108
+ )
109
+
110
+ # Define callables (these are just implementations - LLM decides when to call them)
111
+ tools = {
112
+ "get_option_chain" => {
113
+ tool: option_chain_tool,
114
+ callable: lambda do |symbol:, exchange_segment:, expiry: nil|
115
+ # Normalize empty string to nil (LLM might pass "" when expiry is optional)
116
+ expiry = nil if expiry.is_a?(String) && expiry.empty?
117
+ puts "\n [TOOL EXECUTION] get_option_chain called by Executor"
118
+ puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}, expiry=#{expiry || 'nil'}"
119
+ puts " Note: This is called AFTER LLM decided to use this tool!"
120
+ result = DhanHQDataTools.get_option_chain(
121
+ symbol: symbol.to_s,
122
+ exchange_segment: exchange_segment.to_s,
123
+ expiry: expiry
124
+ )
125
+ if result[:error]
126
+ { error: result[:error] }
127
+ elsif result[:result] && result[:result][:expiries]
128
+ # Return expiry list
129
+ {
130
+ symbol: symbol,
131
+ expiries_available: result[:result][:expiries],
132
+ count: result[:result][:count]
133
+ }
134
+ elsif result[:result] && result[:result][:chain]
135
+ # Return option chain data
136
+ chain = result[:result][:chain]
137
+ strikes = chain.is_a?(Hash) ? chain.keys.sort_by(&:to_f) : []
138
+ {
139
+ symbol: symbol,
140
+ expiry: result[:result][:expiry],
141
+ underlying_price: result[:result][:underlying_last_price],
142
+ strikes_count: strikes.length,
143
+ sample_strikes: strikes.first(5)
144
+ }
145
+ else
146
+ { error: "Unexpected response format" }
147
+ end
148
+ rescue StandardError => e
149
+ { error: e.message }
150
+ end
151
+ },
152
+
153
+ "get_market_quote" => {
154
+ tool: market_quote_tool,
155
+ callable: lambda do |symbol:, exchange_segment:|
156
+ puts "\n [TOOL EXECUTION] get_market_quote called by Executor"
157
+ puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
158
+ puts " Note: This is called AFTER LLM decided to use this tool!"
159
+ result = DhanHQDataTools.get_market_quote(
160
+ symbol: symbol.to_s,
161
+ exchange_segment: exchange_segment.to_s
162
+ )
163
+ if result[:error]
164
+ { error: result[:error] }
165
+ else
166
+ quote = result[:result][:quote]
167
+ {
168
+ symbol: symbol,
169
+ last_price: quote[:last_price],
170
+ volume: quote[:volume],
171
+ ohlc: quote[:ohlc]
172
+ }
173
+ end
174
+ end
175
+ },
176
+
177
+ "get_live_ltp" => {
178
+ tool: live_ltp_tool,
179
+ callable: lambda do |symbol:, exchange_segment:|
180
+ puts "\n [TOOL EXECUTION] get_live_ltp called by Executor"
181
+ puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
182
+ puts " Note: This is called AFTER LLM decided to use this tool!"
183
+ sleep(1.2) # Rate limiting
184
+ result = DhanHQDataTools.get_live_ltp(
185
+ symbol: symbol.to_s,
186
+ exchange_segment: exchange_segment.to_s
187
+ )
188
+ if result[:error]
189
+ { error: result[:error] }
190
+ else
191
+ {
192
+ symbol: symbol,
193
+ ltp: result[:result][:ltp]
194
+ }
195
+ end
196
+ end
197
+ }
198
+ }
199
+
200
+ puts "--- Step 1: Show what tools are available to LLM ---"
201
+ puts "Tools defined:"
202
+ puts " - get_market_quote: Get market quote"
203
+ puts " - get_live_ltp: Get live price"
204
+ puts " - get_option_chain: Get option chain for indices (NIFTY, SENSEX, BANKNIFTY)"
205
+ puts "\nThese tool DEFINITIONS are sent to LLM (not executed yet)\n"
206
+
207
+ puts "--- Step 2: LLM receives query and DECIDES which tools to call ---"
208
+ puts "User query: 'Get RELIANCE quote, NIFTY price, and SENSEX option chain'\n"
209
+ puts "LLM will analyze this and decide to call:"
210
+ puts " 1. get_market_quote(RELIANCE, NSE_EQ)"
211
+ puts " 2. get_live_ltp(NIFTY, IDX_I)"
212
+ puts " 3. get_option_chain(SENSEX, IDX_I)"
213
+ puts "\nThis decision is made by the LLM, not by your code!\n"
214
+
215
+ puts "--- Step 3: Executor sends request to LLM with tool definitions ---"
216
+ puts "Sending to LLM via chat_raw() with tools parameter...\n"
217
+
218
+ # Create executor
219
+ executor = Ollama::Agent::Executor.new(
220
+ client,
221
+ tools: tools,
222
+ max_steps: 10
223
+ )
224
+
225
+ begin
226
+ result = executor.run(
227
+ system: "You are a market data assistant. Use the available tools to get market data. " \
228
+ "For option chains, you can get SENSEX options using get_option_chain with " \
229
+ "symbol='SENSEX' and exchange_segment='IDX_I'.",
230
+ user: "Get market quote for RELIANCE stock, check NIFTY's current price, and get SENSEX option chain"
231
+ )
232
+
233
+ puts "\n--- Step 4: Final result from LLM (after tool execution) ---"
234
+ puts "=" * 60
235
+ puts result
236
+ puts "=" * 60
237
+ rescue Ollama::Error => e
238
+ puts "\n❌ Error: #{e.message}"
239
+ rescue StandardError => e
240
+ puts "\n❌ Unexpected error: #{e.message}"
241
+ end
242
+
243
+ puts "\n--- Summary ---"
244
+ puts "✅ This IS real tool calling:"
245
+ puts " - LLM decides which tools to call (not your code)"
246
+ puts " - LLM returns tool_calls in response"
247
+ puts " - Executor detects tool_calls and executes callables"
248
+ puts " - Tool results fed back to LLM automatically"
249
+ puts " - LLM generates final answer based on tool results"
250
+ puts "\nThe callables are just implementations - the LLM decides WHEN to call them!"
251
+ puts "\n=== DONE ===\n"
@@ -26,9 +26,7 @@ module DhanHQ
26
26
  return nil if value.nil?
27
27
 
28
28
  # If already a number, return as-is (but validate it's reasonable)
29
- if value.is_a?(Numeric)
30
- return value
31
- end
29
+ return value if value.is_a?(Numeric)
32
30
 
33
31
  # If string, clean and convert
34
32
  return nil unless value.is_a?(String)
@@ -38,28 +36,25 @@ module DhanHQ
38
36
  return nil if cleaned.empty?
39
37
 
40
38
  # Try to convert to float (for prices) or integer (for quantity)
41
- result = if cleaned.include?(".")
42
- cleaned.to_f
43
- else
44
- cleaned.to_i
45
- end
46
-
47
- result
39
+ if cleaned.include?(".")
40
+ cleaned.to_f
41
+ else
42
+ cleaned.to_i
43
+ end
48
44
  rescue StandardError
49
45
  nil
50
46
  end
51
47
 
52
48
  # Validates if a price value seems reasonable (not obviously wrong)
53
- def self.validate_price(price, context_hint = nil)
49
+ def self.valid_price?(price, context_hint = nil)
54
50
  return false if price.nil?
55
51
 
56
52
  # If price is suspiciously low (< 10), it might be wrong
57
53
  # But we can't be too strict since some stocks might legitimately be < 10
58
- if price.is_a?(Numeric) && price > 0 && price < 10
59
- # Check if context hint suggests a higher price
60
- if context_hint && context_hint.is_a?(Numeric) && context_hint > price * 100
61
- return false # Likely wrong
62
- end
54
+ # Check if context hint suggests a higher price
55
+ if price.is_a?(Numeric) && price.positive? && price < 10 &&
56
+ context_hint.is_a?(Numeric) && context_hint > price * 100
57
+ return false # Likely wrong
63
58
  end
64
59
 
65
60
  true
@@ -71,7 +66,7 @@ module DhanHQ
71
66
  value.to_s
72
67
  end
73
68
 
74
- private_class_method :normalize_numeric, :normalize_security_id
69
+ private_class_method :normalize_numeric, :normalize_security_id, :valid_price?
75
70
  end
76
71
  end
77
72
  end
@@ -76,7 +76,7 @@ class DataAgent
76
76
  "type" => "number",
77
77
  "minimum" => 0,
78
78
  "maximum" => 1,
79
- "description" => "Confidence in this decision"
79
+ "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
80
80
  },
81
81
  "parameters" => {
82
82
  "type" => "object",
@@ -304,7 +304,7 @@ class TradingAgent
304
304
  "type" => "number",
305
305
  "minimum" => 0,
306
306
  "maximum" => 1,
307
- "description" => "Confidence in this decision"
307
+ "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
308
308
  },
309
309
  "parameters" => {
310
310
  "type" => "object",
@@ -443,6 +443,139 @@ class TradingAgent
443
443
  end
444
444
  end
445
445
 
446
+ def price_range_stats(price_ranges)
447
+ return nil unless price_ranges.is_a?(Array) && price_ranges.any?
448
+
449
+ {
450
+ min: price_ranges.min.round(2),
451
+ max: price_ranges.max.round(2),
452
+ avg: (price_ranges.sum / price_ranges.length).round(2),
453
+ count: price_ranges.length
454
+ }
455
+ end
456
+
457
+ def build_expired_options_summary(stats)
458
+ {
459
+ data_points: stats[:data_points] || 0,
460
+ avg_volume: stats[:avg_volume]&.round(2),
461
+ avg_open_interest: stats[:avg_open_interest]&.round(2),
462
+ avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
463
+ price_range_stats: price_range_stats(stats[:price_ranges]),
464
+ has_ohlc: stats[:has_ohlc],
465
+ has_volume: stats[:has_volume],
466
+ has_open_interest: stats[:has_open_interest],
467
+ has_implied_volatility: stats[:has_implied_volatility]
468
+ }
469
+ end
470
+
471
+ def build_option_chain_summary(chain_result)
472
+ chain = chain_result[:result][:chain]
473
+ underlying_price = chain_result[:result][:underlying_last_price]
474
+
475
+ unless chain.is_a?(Hash)
476
+ return [{ expiry: chain_result[:result][:expiry], chain_type: chain.class },
477
+ underlying_price]
478
+ end
479
+
480
+ strike_prices = chain.keys.sort_by(&:to_f)
481
+ first_strike_data = strike_prices.any? ? chain[strike_prices.first] : nil
482
+ atm_strike = select_atm_strike(strike_prices, underlying_price)
483
+ atm_data = atm_strike ? chain[atm_strike] : nil
484
+ sample_greeks = build_sample_greeks(atm_data, atm_strike)
485
+
486
+ summary = {
487
+ expiry: chain_result[:result][:expiry],
488
+ underlying_last_price: underlying_price,
489
+ strikes_count: strike_prices.length,
490
+ has_call_options: option_type_present?(first_strike_data, "ce"),
491
+ has_put_options: option_type_present?(first_strike_data, "pe"),
492
+ has_greeks: sample_greeks.any?,
493
+ strike_range: strike_range_summary(strike_prices),
494
+ sample_greeks: sample_greeks.any? ? sample_greeks : nil
495
+ }
496
+
497
+ [summary, underlying_price]
498
+ end
499
+
500
+ def select_atm_strike(strike_prices, underlying_price)
501
+ return strike_prices.first unless underlying_price && strike_prices.any?
502
+
503
+ strike_prices.min_by { |strike| (strike.to_f - underlying_price).abs }
504
+ end
505
+
506
+ def option_type_present?(strike_data, key)
507
+ strike_data.is_a?(Hash) && (strike_data.key?(key) || strike_data.key?(key.to_sym))
508
+ end
509
+
510
+ def strike_range_summary(strike_prices)
511
+ return nil if strike_prices.empty?
512
+
513
+ {
514
+ min: strike_prices.first,
515
+ max: strike_prices.last,
516
+ sample_strikes: strike_prices.first(5)
517
+ }
518
+ end
519
+
520
+ def build_sample_greeks(atm_data, atm_strike)
521
+ return {} unless atm_data.is_a?(Hash)
522
+
523
+ sample = {}
524
+ call_data = atm_data["ce"] || atm_data[:ce]
525
+ put_data = atm_data["pe"] || atm_data[:pe]
526
+
527
+ call_greeks = extract_greeks(call_data)
528
+ sample[:call] = greeks_summary(call_greeks, call_data, atm_strike) if call_greeks
529
+
530
+ put_greeks = extract_greeks(put_data)
531
+ sample[:put] = greeks_summary(put_greeks, put_data, atm_strike) if put_greeks
532
+
533
+ sample
534
+ end
535
+
536
+ def extract_greeks(option_data)
537
+ return nil unless option_data.is_a?(Hash)
538
+ return nil unless option_data.key?("greeks") || option_data.key?(:greeks)
539
+
540
+ option_data["greeks"] || option_data[:greeks]
541
+ end
542
+
543
+ def greeks_summary(greeks, option_data, atm_strike)
544
+ {
545
+ strike: atm_strike,
546
+ delta: greeks["delta"] || greeks[:delta],
547
+ theta: greeks["theta"] || greeks[:theta],
548
+ gamma: greeks["gamma"] || greeks[:gamma],
549
+ vega: greeks["vega"] || greeks[:vega],
550
+ iv: option_data["implied_volatility"] || option_data[:implied_volatility],
551
+ oi: option_data["oi"] || option_data[:oi],
552
+ last_price: option_data["last_price"] || option_data[:last_price]
553
+ }
554
+ end
555
+
556
+ def format_score_breakdown(details)
557
+ "Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
558
+ "Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
559
+ end
560
+
561
+ def format_option_setup_details(setup)
562
+ iv = setup[:iv]&.round(2) || "N/A"
563
+ oi = setup[:oi] || "N/A"
564
+ volume = setup[:volume] || "N/A"
565
+ "IV: #{iv}% | OI: #{oi} | Volume: #{volume}"
566
+ end
567
+
568
+ def handle_option_chain_result(chain_result)
569
+ if chain_result[:result] && chain_result[:result][:chain]
570
+ chain_summary, underlying_price = build_option_chain_summary(chain_result)
571
+ puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
572
+ puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
573
+ puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
574
+ elsif chain_result[:error]
575
+ puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
576
+ end
577
+ end
578
+
446
579
  # Main execution
447
580
  if __FILE__ == $PROGRAM_NAME
448
581
  # Configure DhanHQ (must be done before using DhanHQ models)
@@ -529,16 +662,26 @@ if __FILE__ == $PROGRAM_NAME
529
662
  puts " ⚠️ RELIANCE data error: #{e.message}"
530
663
  end
531
664
 
532
- # NOTE: Positions and holdings are not part of the 6 Data APIs
665
+ # NOTE: Positions and holdings are not part of the 6 Data APIs, but available via DhanHQ gem
533
666
  begin
534
- positions_result = { action: "check_positions", result: { positions: [], count: 0 },
535
- note: "Positions API not available in Data Tools" }
536
- if positions_result[:result]
537
- market_data[:positions] = positions_result[:result][:positions] || []
538
- puts " ✅ Positions: #{positions_result[:result][:count] || 0} active"
539
- else
540
- puts " ✅ Positions: 0 active (Positions API not in Data Tools)"
541
- market_data[:positions] = []
667
+ positions_list = DhanHQ::Models::Position.all
668
+ positions_data = positions_list.map do |pos|
669
+ {
670
+ trading_symbol: pos.trading_symbol,
671
+ quantity: pos.net_qty,
672
+ average_price: pos.buy_avg,
673
+ exchange_segment: pos.exchange_segment,
674
+ security_id: pos.security_id,
675
+ pnl: pos.realized_profit
676
+ }
677
+ end
678
+ market_data[:positions] = positions_data
679
+ puts " ✅ Positions: #{positions_data.length} active"
680
+
681
+ if positions_data.any?
682
+ positions_data.each do |pos|
683
+ puts " - #{pos[:trading_symbol]}: Qty #{pos[:quantity]} @ ₹#{pos[:average_price]}"
684
+ end
542
685
  end
543
686
  rescue StandardError => e
544
687
  puts " ⚠️ Positions error: #{e.message}"
@@ -661,7 +804,7 @@ if __FILE__ == $PROGRAM_NAME
661
804
  puts " ✅ Historical data retrieved"
662
805
  puts " 📊 Type: #{result[:type]}"
663
806
  puts " 📊 Records: #{result[:result][:count]}"
664
- if result[:result][:count] == 0
807
+ if result[:result][:count].zero?
665
808
  puts " ⚠️ No data found for date range #{from_date} to #{to_date}"
666
809
  puts " (This may be normal if market was closed or data unavailable)"
667
810
  end
@@ -698,27 +841,7 @@ if __FILE__ == $PROGRAM_NAME
698
841
  # Show concise summary of expired options data instead of full data (can be very large)
699
842
  if result[:result][:summary_stats]
700
843
  stats = result[:result][:summary_stats]
701
- # Create a concise summary without the huge price_ranges array
702
- concise_summary = {
703
- data_points: stats[:data_points] || 0,
704
- avg_volume: stats[:avg_volume]&.round(2),
705
- avg_open_interest: stats[:avg_open_interest]&.round(2),
706
- avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
707
- price_range_stats: if stats[:price_ranges]&.is_a?(Array) && !stats[:price_ranges].empty?
708
- {
709
- min: stats[:price_ranges].min.round(2),
710
- max: stats[:price_ranges].max.round(2),
711
- avg: (stats[:price_ranges].sum / stats[:price_ranges].length).round(2),
712
- count: stats[:price_ranges].length
713
- }
714
- else
715
- nil
716
- end,
717
- has_ohlc: stats[:has_ohlc],
718
- has_volume: stats[:has_volume],
719
- has_open_interest: stats[:has_open_interest],
720
- has_implied_volatility: stats[:has_implied_volatility]
721
- }
844
+ concise_summary = build_expired_options_summary(stats)
722
845
  puts " 📊 Data summary: #{JSON.pretty_generate(concise_summary)}"
723
846
  else
724
847
  puts " 📊 Data available but summary stats not found"
@@ -739,8 +862,8 @@ if __FILE__ == $PROGRAM_NAME
739
862
  begin
740
863
  # NOTE: Options symbols may need different format
741
864
  # Try with NIFTY which typically has options
742
- # First, get the list of available expiries
743
- expiry_list_result = DhanHQDataTools.get_option_chain(
865
+ # First, get the list of available expiries using get_expiry_list
866
+ expiry_list_result = DhanHQDataTools.get_expiry_list(
744
867
  symbol: "NIFTY", # NIFTY typically has options, RELIANCE might not
745
868
  exchange_segment: "IDX_I"
746
869
  )
@@ -759,87 +882,7 @@ if __FILE__ == $PROGRAM_NAME
759
882
  exchange_segment: "IDX_I", # Use IDX_I for index options underlying
760
883
  expiry: next_expiry
761
884
  )
762
- if chain_result[:result] && chain_result[:result][:chain]
763
- chain = chain_result[:result][:chain]
764
- underlying_price = chain_result[:result][:underlying_last_price]
765
-
766
- # Option chain structure: chain[:oc] is a hash with strike prices as string keys
767
- # Each strike contains "ce" (call) and "pe" (put) data with Greeks, IV, OI, etc.
768
- chain_summary = if chain.is_a?(Hash)
769
- # Strike prices are string keys (e.g., "25000.000000")
770
- strike_prices = chain.keys.sort_by { |k| k.to_f }
771
- first_strike_data = chain[strike_prices.first] unless strike_prices.empty?
772
-
773
- # Find ATM or nearest strike to underlying price for sample Greeks
774
- atm_strike = if underlying_price && !strike_prices.empty?
775
- strike_prices.min_by { |s| (s.to_f - underlying_price).abs }
776
- else
777
- strike_prices.first
778
- end
779
- atm_data = chain[atm_strike] if atm_strike
780
-
781
- # Extract Greeks from ATM strike if available
782
- sample_greeks = {}
783
- if atm_data.is_a?(Hash)
784
- ce_data = atm_data["ce"] || atm_data[:ce]
785
- pe_data = atm_data["pe"] || atm_data[:pe]
786
-
787
- if ce_data.is_a?(Hash) && (ce_data.key?("greeks") || ce_data.key?(:greeks))
788
- ce_greeks = ce_data["greeks"] || ce_data[:greeks]
789
- sample_greeks[:call] = {
790
- strike: atm_strike,
791
- delta: ce_greeks["delta"] || ce_greeks[:delta],
792
- theta: ce_greeks["theta"] || ce_greeks[:theta],
793
- gamma: ce_greeks["gamma"] || ce_greeks[:gamma],
794
- vega: ce_greeks["vega"] || ce_greeks[:vega],
795
- iv: ce_data["implied_volatility"] || ce_data[:implied_volatility],
796
- oi: ce_data["oi"] || ce_data[:oi],
797
- last_price: ce_data["last_price"] || ce_data[:last_price]
798
- }
799
- end
800
-
801
- if pe_data.is_a?(Hash) && (pe_data.key?("greeks") || pe_data.key?(:greeks))
802
- pe_greeks = pe_data["greeks"] || pe_data[:greeks]
803
- sample_greeks[:put] = {
804
- strike: atm_strike,
805
- delta: pe_greeks["delta"] || pe_greeks[:delta],
806
- theta: pe_greeks["theta"] || pe_greeks[:theta],
807
- gamma: pe_greeks["gamma"] || pe_greeks[:gamma],
808
- vega: pe_greeks["vega"] || pe_greeks[:vega],
809
- iv: pe_data["implied_volatility"] || pe_data[:implied_volatility],
810
- oi: pe_data["oi"] || pe_data[:oi],
811
- last_price: pe_data["last_price"] || pe_data[:last_price]
812
- }
813
- end
814
- end
815
-
816
- {
817
- expiry: chain_result[:result][:expiry],
818
- underlying_last_price: underlying_price,
819
- strikes_count: strike_prices.length,
820
- has_call_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("ce") || first_strike_data.key?(:ce)),
821
- has_put_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("pe") || first_strike_data.key?(:pe)),
822
- has_greeks: !sample_greeks.empty?,
823
- strike_range: if strike_prices.empty?
824
- nil
825
- else
826
- {
827
- min: strike_prices.first,
828
- max: strike_prices.last,
829
- sample_strikes: strike_prices.first(5)
830
- }
831
- end,
832
- sample_greeks: sample_greeks.empty? ? nil : sample_greeks
833
- }
834
- else
835
- { expiry: chain_result[:result][:expiry], chain_type: chain.class }
836
- end
837
- puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
838
- puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
839
- puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
840
- elsif chain_result[:error]
841
- puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
842
- end
885
+ handle_option_chain_result(chain_result)
843
886
  end
844
887
  elsif expiry_list_result[:error]
845
888
  puts " ⚠️ #{expiry_list_result[:error]}"