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
@@ -17,9 +17,7 @@ module DhanHQ
17
17
  # Analyze underlying
18
18
  underlying_analysis = analyze_underlying(underlying_symbol, exchange_segment)
19
19
  if underlying_analysis[:error]
20
- error_msg = "Failed to analyze underlying: #{underlying_analysis[:error]}"
21
- puts " ⚠️ #{error_msg}" if verbose
22
- return { error: error_msg }
20
+ return error_result("Failed to analyze underlying: #{underlying_analysis[:error]}", verbose)
23
21
  end
24
22
 
25
23
  puts " ✅ Underlying analysis complete" if verbose
@@ -27,20 +25,11 @@ module DhanHQ
27
25
  # Get option chain - first get expiry list, then fetch chain for first expiry
28
26
  expiry_list_result = get_option_chain(underlying_symbol, exchange_segment)
29
27
  if expiry_list_result[:error]
30
- error_msg = "Failed to get expiry list: #{expiry_list_result[:error]}"
31
- puts " ⚠️ #{error_msg}" if verbose
32
- return { error: error_msg }
28
+ return error_result("Failed to get expiry list: #{expiry_list_result[:error]}", verbose)
33
29
  end
34
30
 
35
- # Extract expiry list
36
- expiry_list = expiry_list_result[:result] || expiry_list_result["result"]
37
- expiries = expiry_list[:expiries] || expiry_list["expiries"] if expiry_list.is_a?(Hash)
38
-
39
- unless expiries && expiries.is_a?(Array) && !expiries.empty?
40
- error_msg = "No expiries found in option chain"
41
- puts " ⚠️ #{error_msg}" if verbose
42
- return { error: error_msg }
43
- end
31
+ expiries = extract_expiries(expiry_list_result)
32
+ return error_result("No expiries found in option chain", verbose) unless expiries
44
33
 
45
34
  # Get chain for first expiry (next expiry)
46
35
  next_expiry = expiries.first
@@ -48,9 +37,7 @@ module DhanHQ
48
37
 
49
38
  option_chain = get_option_chain(underlying_symbol, exchange_segment, expiry: next_expiry)
50
39
  if option_chain[:error]
51
- error_msg = "Failed to get option chain for expiry #{next_expiry}: #{option_chain[:error]}"
52
- puts " ⚠️ #{error_msg}" if verbose
53
- return { error: error_msg }
40
+ return error_result("Failed to get option chain for expiry #{next_expiry}: #{option_chain[:error]}", verbose)
54
41
  end
55
42
 
56
43
  puts " ✅ Option chain retrieved for expiry: #{next_expiry}" if verbose
@@ -107,280 +94,398 @@ module DhanHQ
107
94
  end
108
95
 
109
96
  def find_options_setups(analysis, option_chain, min_score: 50, verbose: false)
110
- setups = []
111
- rejected = []
112
- strikes_evaluated = 0
113
- strikes_within_range = 0
114
-
115
- if verbose
116
- puts " 📊 Underlying Analysis:"
117
- if analysis && !analysis.empty?
118
- current_price = analysis[:current_price]
119
- trend = analysis[:trend]&.dig(:trend)
120
- rsi = analysis[:indicators]&.dig(:rsi)
121
- puts " Current Price: #{current_price || 'N/A'}"
122
- puts " Trend: #{trend || 'N/A'}"
123
- puts " RSI: #{rsi&.round(2) || 'N/A'}"
124
- else
125
- puts " ⚠️ Analysis data not available"
126
- end
127
- end
97
+ tracking = {
98
+ setups: [],
99
+ rejected: [],
100
+ strikes_evaluated: 0,
101
+ strikes_within_range: 0
102
+ }
128
103
 
129
- # Debug: Check the structure
130
- if verbose
131
- puts " 🔍 Option chain structure:"
132
- puts " Keys: #{option_chain.keys.inspect}" if option_chain.is_a?(Hash)
133
- puts " Has result?: #{option_chain.key?(:result) || option_chain.key?('result')}"
134
- if option_chain[:result] || option_chain["result"]
135
- result = option_chain[:result] || option_chain["result"]
136
- puts " Result keys: #{result.keys.inspect}" if result.is_a?(Hash)
137
- puts " Has chain?: #{result.key?(:chain) || result.key?('chain')}"
138
- puts " Has expiries?: #{result.key?(:expiries) || result.key?('expiries')}"
139
- end
104
+ log_underlying_analysis(analysis, verbose)
105
+ log_option_chain_debug(option_chain, verbose)
106
+
107
+ chain = extract_option_chain(option_chain, verbose)
108
+ return [] unless chain
109
+
110
+ context = build_analysis_context(analysis)
111
+ log_chain_summary(chain, context[:trend], verbose)
112
+
113
+ evaluation_context = {
114
+ tracking: tracking,
115
+ context: context,
116
+ min_score: min_score,
117
+ verbose: verbose
118
+ }
119
+
120
+ chain.each do |strike_key, strike_data|
121
+ strike = strike_key.to_f
122
+ next unless strike_within_range?(strike, context[:current_price])
123
+
124
+ tracking[:strikes_within_range] += 1
125
+ tracking[:strikes_evaluated] += 1
126
+
127
+ evaluate_strike_options(
128
+ strike: strike,
129
+ strike_data: strike_data,
130
+ evaluation_context: evaluation_context
131
+ )
140
132
  end
141
133
 
142
- # Extract chain from result
134
+ log_evaluation_summary(tracking, context, min_score, verbose)
135
+
136
+ tracking[:setups].sort_by { |setup| -setup[:score] }.first(5)
137
+ end
138
+
139
+ def calculate_options_score(option_data, context)
140
+ score = 0
141
+ score += implied_volatility_points(option_data[:implied_volatility])
142
+ score += open_interest_points(option_data[:open_interest])
143
+ score += volume_points(option_data[:volume])
144
+ score += trend_points(option_data[:type], context[:trend], context[:relative_strength_index])
145
+ score += rsi_points(option_data[:type], context[:relative_strength_index])
146
+ score
147
+ end
148
+
149
+ def implied_volatility_points(implied_volatility)
150
+ return 0 unless implied_volatility
151
+
152
+ return 30 if implied_volatility < 15
153
+ return 20 if implied_volatility < 25
154
+ return 10 if implied_volatility < 35
155
+
156
+ 0
157
+ end
158
+
159
+ def open_interest_points(open_interest)
160
+ return 0 unless open_interest
161
+ return 25 if open_interest > 1_000_000
162
+ return 15 if open_interest > 500_000
163
+ return 10 if open_interest > 100_000
164
+
165
+ 0
166
+ end
167
+
168
+ def volume_points(volume)
169
+ return 0 unless volume
170
+ return 20 if volume > 10_000
171
+ return 10 if volume > 5_000
172
+
173
+ 0
174
+ end
175
+
176
+ def trend_points(option_type, trend, relative_strength_index)
177
+ return 0 unless trend
178
+ return 15 if trend_alignment?(option_type, trend)
179
+ return 0 unless trend == :sideways
180
+
181
+ rsi_bias_points(option_type, relative_strength_index)
182
+ end
183
+
184
+ def trend_alignment?(option_type, trend)
185
+ (option_type == :call && trend == :uptrend) || (option_type == :put && trend == :downtrend)
186
+ end
187
+
188
+ def rsi_bias_points(option_type, relative_strength_index)
189
+ return 0 unless relative_strength_index
190
+ return 10 if option_type == :call && relative_strength_index > 50
191
+ return 10 if option_type == :put && relative_strength_index < 50
192
+
193
+ 0
194
+ end
195
+
196
+ def rsi_points(option_type, relative_strength_index)
197
+ return 0 unless relative_strength_index
198
+ return 10 if option_type == :call && relative_strength_index.between?(50, 70)
199
+ return 10 if option_type == :put && relative_strength_index.between?(30, 50)
200
+
201
+ 0
202
+ end
203
+
204
+ def error_result(message, verbose)
205
+ puts " ⚠️ #{message}" if verbose
206
+ { error: message }
207
+ end
208
+
209
+ def extract_expiries(expiry_list_result)
210
+ expiry_list = expiry_list_result[:result] || expiry_list_result["result"]
211
+ return nil unless expiry_list.is_a?(Hash)
212
+
213
+ expiries = expiry_list[:expiries] || expiry_list["expiries"]
214
+ return nil unless expiries.is_a?(Array) && expiries.any?
215
+
216
+ expiries
217
+ end
218
+
219
+ def log_underlying_analysis(analysis, verbose)
220
+ return unless verbose
221
+
222
+ puts " 📊 Underlying Analysis:"
223
+ return puts(" ⚠️ Analysis data not available") unless analysis && !analysis.empty?
224
+
225
+ current_price = analysis[:current_price]
226
+ trend = analysis[:trend]&.dig(:trend)
227
+ relative_strength_index = analysis[:indicators]&.dig(:rsi)
228
+ puts " Current Price: #{current_price || 'N/A'}"
229
+ puts " Trend: #{trend || 'N/A'}"
230
+ puts " RSI: #{relative_strength_index&.round(2) || 'N/A'}"
231
+ end
232
+
233
+ def log_option_chain_debug(option_chain, verbose)
234
+ return unless verbose
235
+
236
+ puts " 🔍 Option chain structure:"
237
+ return unless option_chain.is_a?(Hash)
238
+
239
+ puts " Keys: #{option_chain.keys.inspect}"
240
+ puts " Has result?: #{option_chain.key?(:result) || option_chain.key?('result')}"
143
241
  result = option_chain[:result] || option_chain["result"]
242
+ return unless result.is_a?(Hash)
243
+
244
+ puts " Result keys: #{result.keys.inspect}"
245
+ puts " Has chain?: #{result.key?(:chain) || result.key?('chain')}"
246
+ puts " Has expiries?: #{result.key?(:expiries) || result.key?('expiries')}"
247
+ end
144
248
 
249
+ def extract_option_chain(option_chain, verbose)
250
+ result = option_chain[:result] || option_chain["result"]
145
251
  unless result
146
252
  puts " ⚠️ Option chain data not available or invalid (no result)" if verbose
147
- return setups
253
+ return nil
148
254
  end
149
255
 
150
256
  chain = result[:chain] || result["chain"]
257
+ return chain if chain
151
258
 
152
- unless chain
153
- puts " ⚠️ Option chain data not available or invalid (no chain in result)" if verbose
154
- if verbose
155
- puts " Available keys in result: #{result.keys.inspect}" if result.is_a?(Hash)
156
- puts " Result structure: #{result.inspect[0..200]}"
157
- end
158
- return setups
159
- end
160
- current_price = analysis[:current_price] if analysis
161
- trend = analysis[:trend]&.dig(:trend) if analysis
162
- rsi = analysis[:indicators]&.dig(:rsi) if analysis
163
-
164
- if verbose
165
- puts " Chain strikes: #{chain.keys.length}"
166
- puts " Looking for: #{if trend == :uptrend
167
- 'CALL options'
168
- else
169
- trend == :downtrend ? 'PUT options' : 'CALL or PUT (sideways trend)'
170
- end}"
171
- end
259
+ return nil unless verbose
172
260
 
173
- # For intraday options buying:
174
- # - Look for ATM or near-ATM strikes
175
- # - Prefer calls in uptrend, puts in downtrend
176
- # - Consider IV (lower is better for buying)
177
- # - Look for high volume/OI
178
-
179
- chain.each do |strike_str, strike_data|
180
- strike = strike_str.to_f
181
- price_diff_pct = ((strike - current_price).abs / current_price * 100).round(2)
182
-
183
- next unless (strike - current_price).abs / current_price < 0.02 # Within 2% of current price
184
-
185
- strikes_within_range += 1
186
-
187
- strikes_evaluated += 1
188
-
189
- ce_data = strike_data["ce"] || strike_data[:ce]
190
- pe_data = strike_data["pe"] || strike_data[:pe]
191
-
192
- # Evaluate CALL options
193
- if ce_data && (trend == :uptrend || trend == :sideways)
194
- iv = ce_data["implied_volatility"] || ce_data[:implied_volatility]
195
- oi = ce_data["oi"] || ce_data[:oi]
196
- volume = ce_data["volume"] || ce_data[:volume]
197
-
198
- score = calculate_options_score(iv, oi, volume, :call, trend, rsi)
199
- if score >= min_score
200
- setups << {
201
- type: :call,
202
- strike: strike,
203
- iv: iv,
204
- oi: oi,
205
- volume: volume,
206
- score: score,
207
- recommendation: if score > 70
208
- "Strong buy"
209
- else
210
- score > 50 ? "Moderate buy" : "Weak"
211
- end
212
- }
213
- elsif verbose
214
- rejected << { type: :call, strike: strike, score: score, reason: "Below min_score (#{min_score})" }
215
- end
216
- end
217
-
218
- # Evaluate PUT options
219
- if pe_data && (trend == :downtrend || trend == :sideways)
220
- iv = pe_data["implied_volatility"] || pe_data[:implied_volatility]
221
- oi = pe_data["oi"] || pe_data[:oi]
222
- volume = pe_data["volume"] || pe_data[:volume]
223
-
224
- score = calculate_options_score(iv, oi, volume, :put, trend, rsi)
225
- if score >= min_score
226
- setups << {
227
- type: :put,
228
- strike: strike,
229
- iv: iv,
230
- oi: oi,
231
- volume: volume,
232
- score: score,
233
- recommendation: if score > 70
234
- "Strong buy"
235
- else
236
- score > 50 ? "Moderate buy" : "Weak"
237
- end
238
- }
239
- elsif verbose
240
- rejected << { type: :put, strike: strike, score: score, reason: "Below min_score (#{min_score})" }
241
- end
242
- end
243
- end
261
+ puts " ⚠️ Option chain data not available or invalid (no chain in result)"
262
+ puts " Available keys in result: #{result.keys.inspect}" if result.is_a?(Hash)
263
+ puts " Result structure: #{result.inspect[0..200]}"
264
+ nil
265
+ end
244
266
 
245
- if verbose
246
- puts " 📊 Evaluation Summary:"
247
- puts " Strikes within 2% of price: #{strikes_within_range}"
248
- puts " Strikes evaluated: #{strikes_evaluated}"
249
- puts " Setups found: #{setups.length}"
250
-
251
- if !rejected.empty?
252
- puts " 📋 Rejected setups: #{rejected.length} (below min_score #{min_score})"
253
- rejected.first(5).each do |r|
254
- puts " ❌ #{r[:type].to_s.upcase} @ #{r[:strike]}: Score #{r[:score]}/100"
255
- end
256
- elsif strikes_evaluated == 0
257
- if !trend || trend == :sideways
258
- puts " ⚠️ Sideways trend - no clear directional bias for calls/puts"
259
- elsif trend == :uptrend && strikes_within_range == 0
260
- puts " ⚠️ No CALL strikes found within 2% of current price (#{current_price})"
261
- elsif trend == :downtrend && strikes_within_range == 0
262
- puts " ⚠️ No PUT strikes found within 2% of current price (#{current_price})"
263
- elsif strikes_within_range > 0 && strikes_evaluated == 0
264
- puts " ⚠️ Found #{strikes_within_range} strikes within range, but none match trend criteria"
265
- puts " (Trend: #{trend}, looking for #{trend == :uptrend ? 'CALL' : 'PUT'} options)"
266
- else
267
- puts " ⚠️ No suitable strikes found for current trend (#{trend || 'unknown'})"
268
- end
269
- end
270
- end
267
+ def build_analysis_context(analysis)
268
+ return { current_price: nil, trend: nil, relative_strength_index: nil } unless analysis
271
269
 
272
- setups.sort_by { |s| -s[:score] }.first(5) # Top 5 setups
270
+ {
271
+ current_price: analysis[:current_price],
272
+ trend: analysis[:trend]&.dig(:trend),
273
+ relative_strength_index: analysis[:indicators]&.dig(:rsi)
274
+ }
273
275
  end
274
276
 
275
- def calculate_options_score(iv, oi, volume, option_type, trend, rsi)
276
- score = 0
277
+ def log_chain_summary(chain, trend, verbose)
278
+ return unless verbose
277
279
 
278
- # IV scoring (lower is better for buying) - 0-30 points
279
- if iv
280
- if iv < 15
281
- score += 30
282
- elsif iv < 25
283
- score += 20
284
- elsif iv < 35
285
- score += 10
286
- end
287
- end
280
+ puts " Chain strikes: #{chain.keys.length}"
281
+ puts " Looking for: #{preferred_option_label(trend)}"
282
+ end
283
+
284
+ def preferred_option_label(trend)
285
+ return "CALL options" if trend == :uptrend
286
+ return "PUT options" if trend == :downtrend
288
287
 
289
- # OI scoring (higher is better) - 0-25 points
290
- if oi && oi > 1_000_000
291
- score += 25
292
- elsif oi && oi > 500_000
293
- score += 15
294
- elsif oi && oi > 100_000
295
- score += 10
288
+ "CALL or PUT (sideways trend)"
289
+ end
290
+
291
+ def strike_within_range?(strike, current_price)
292
+ return false unless current_price
293
+
294
+ (strike - current_price).abs / current_price < 0.02
295
+ end
296
+
297
+ def evaluate_strike_options(strike:, strike_data:, evaluation_context:)
298
+ context = evaluation_context[:context]
299
+
300
+ call_data = strike_data["ce"] || strike_data[:ce]
301
+ put_data = strike_data["pe"] || strike_data[:pe]
302
+
303
+ if call_data && option_allowed?(:call, context[:trend])
304
+ evaluate_option_setup(
305
+ option_type: :call,
306
+ strike: strike,
307
+ raw_data: call_data,
308
+ evaluation_context: evaluation_context
309
+ )
296
310
  end
297
311
 
298
- # Volume scoring - 0-20 points
299
- if volume && volume > 10_000
300
- score += 20
301
- elsif volume && volume > 5_000
302
- score += 10
312
+ return unless put_data && option_allowed?(:put, context[:trend])
313
+
314
+ evaluate_option_setup(
315
+ option_type: :put,
316
+ strike: strike,
317
+ raw_data: put_data,
318
+ evaluation_context: evaluation_context
319
+ )
320
+ end
321
+
322
+ def option_allowed?(option_type, trend)
323
+ return %i[uptrend sideways].include?(trend) if option_type == :call
324
+ return %i[downtrend sideways].include?(trend) if option_type == :put
325
+
326
+ false
327
+ end
328
+
329
+ def evaluate_option_setup(option_type:, strike:, raw_data:, evaluation_context:)
330
+ tracking = evaluation_context[:tracking]
331
+ context = evaluation_context[:context]
332
+ min_score = evaluation_context[:min_score]
333
+ verbose = evaluation_context[:verbose]
334
+
335
+ option_data = option_data_for(option_type, raw_data)
336
+ score = calculate_options_score(option_data, context)
337
+
338
+ if score >= min_score
339
+ tracking[:setups] << build_setup(option_data, strike, score)
340
+ return
303
341
  end
304
342
 
305
- # Trend alignment - 0-15 points
306
- if trend == :sideways
307
- # In sideways markets, use RSI to bias scoring
308
- if option_type == :call && rsi && rsi > 50
309
- score += 10 # Slight bias toward calls if RSI > 50
310
- elsif option_type == :put && rsi && rsi < 50
311
- score += 10 # Slight bias toward puts if RSI < 50
312
- end
313
- else
314
- score += 15 if (option_type == :call && trend == :uptrend) || (option_type == :put && trend == :downtrend)
343
+ return unless verbose
344
+
345
+ tracking[:rejected] << {
346
+ type: option_type,
347
+ strike: strike,
348
+ score: score,
349
+ reason: "Below min_score (#{min_score})"
350
+ }
351
+ end
352
+
353
+ def option_data_for(option_type, raw_data)
354
+ {
355
+ type: option_type,
356
+ implied_volatility: raw_data["implied_volatility"] || raw_data[:implied_volatility],
357
+ open_interest: raw_data["oi"] || raw_data[:oi],
358
+ volume: raw_data["volume"] || raw_data[:volume],
359
+ last_price: raw_data["last_price"] || raw_data[:last_price] || raw_data["ltp"] || raw_data[:ltp]
360
+ }
361
+ end
362
+
363
+ def build_setup(option_data, strike, score)
364
+ {
365
+ type: option_data[:type],
366
+ strike: strike,
367
+ iv: option_data[:implied_volatility],
368
+ oi: option_data[:open_interest],
369
+ volume: option_data[:volume],
370
+ ltp: option_data[:last_price],
371
+ score: score,
372
+ recommendation: recommendation_for_score(score)
373
+ }
374
+ end
375
+
376
+ def recommendation_for_score(score)
377
+ return "Strong buy" if score > 70
378
+ return "Moderate buy" if score > 50
379
+
380
+ "Weak"
381
+ end
382
+
383
+ def log_evaluation_summary(tracking, context, min_score, verbose)
384
+ return unless verbose
385
+
386
+ puts " 📊 Evaluation Summary:"
387
+ puts " Strikes within 2% of price: #{tracking[:strikes_within_range]}"
388
+ puts " Strikes evaluated: #{tracking[:strikes_evaluated]}"
389
+ puts " Setups found: #{tracking[:setups].length}"
390
+
391
+ if tracking[:rejected].any?
392
+ log_rejected_setups(tracking[:rejected], min_score)
393
+ elsif tracking[:strikes_evaluated].zero?
394
+ log_no_strike_message(tracking, context)
315
395
  end
396
+ end
316
397
 
317
- # RSI alignment - 0-10 points
318
- if rsi
319
- if option_type == :call && rsi < 70 && rsi > 50
320
- score += 10
321
- elsif option_type == :put && rsi > 30 && rsi < 50
322
- score += 10
323
- end
398
+ def log_rejected_setups(rejected, min_score)
399
+ puts " 📋 Rejected setups: #{rejected.length} (below min_score #{min_score})"
400
+ rejected.first(5).each do |rejection|
401
+ puts " ❌ #{rejection[:type].to_s.upcase} @ #{rejection[:strike]}: Score #{rejection[:score]}/100"
324
402
  end
403
+ end
325
404
 
326
- score
405
+ def log_no_strike_message(tracking, context)
406
+ trend = context[:trend]
407
+ current_price = context[:current_price]
408
+
409
+ if !trend || trend == :sideways
410
+ puts " ⚠️ Sideways trend - no clear directional bias for calls/puts"
411
+ elsif trend == :uptrend && tracking[:strikes_within_range].zero?
412
+ puts " ⚠️ No CALL strikes found within 2% of current price (#{current_price})"
413
+ elsif trend == :downtrend && tracking[:strikes_within_range].zero?
414
+ puts " ⚠️ No PUT strikes found within 2% of current price (#{current_price})"
415
+ else
416
+ puts " ⚠️ No suitable strikes found for current trend (#{trend || 'unknown'})"
417
+ end
327
418
  end
328
419
 
329
420
  def convert_to_ohlc(historical_data)
330
421
  return [] unless historical_data.is_a?(Hash)
331
422
 
332
- # Navigate to the actual data: result -> result -> data
423
+ data = extract_data_payload(historical_data)
424
+ return [] unless data
425
+
426
+ return ohlc_from_hash(data) if data.is_a?(Hash)
427
+ return ohlc_from_array(data) if data.is_a?(Array)
428
+
429
+ []
430
+ end
431
+
432
+ def extract_data_payload(historical_data)
333
433
  outer_result = historical_data[:result] || historical_data["result"]
334
- return [] unless outer_result.is_a?(Hash)
434
+ return nil unless outer_result.is_a?(Hash)
335
435
 
336
- data = outer_result[:data] || outer_result["data"]
337
- return [] unless data
436
+ outer_result[:data] || outer_result["data"]
437
+ end
338
438
 
339
- # Handle DhanHQ format: {open: [...], high: [...], low: [...], close: [...], volume: [...]}
340
- if data.is_a?(Hash)
341
- opens = data[:open] || data["open"] || []
342
- highs = data[:high] || data["high"] || []
343
- lows = data[:low] || data["low"] || []
344
- closes = data[:close] || data["close"] || []
345
- volumes = data[:volume] || data["volume"] || []
346
-
347
- return [] if closes.nil? || closes.empty?
348
-
349
- # Convert parallel arrays to array of hashes
350
- max_length = [opens.length, highs.length, lows.length, closes.length].max
351
- return [] if max_length.zero?
352
-
353
- ohlc_data = []
354
-
355
- (0...max_length).each do |i|
356
- ohlc_data << {
357
- open: opens[i] || closes[i] || 0,
358
- high: highs[i] || closes[i] || 0,
359
- low: lows[i] || closes[i] || 0,
360
- close: closes[i] || 0,
361
- volume: volumes[i] || 0
362
- }
363
- end
364
-
365
- return ohlc_data
366
- end
439
+ def ohlc_from_hash(data)
440
+ series = extract_series(data)
441
+ return [] if series[:closes].nil? || series[:closes].empty?
442
+
443
+ max_length = series_lengths(series).max
444
+ return [] if max_length.zero?
367
445
 
368
- # Handle array format: [{open, high, low, close, volume}, ...]
369
- if data.is_a?(Array)
370
- return data.map do |bar|
371
- next nil unless bar.is_a?(Hash)
372
-
373
- {
374
- open: bar["open"] || bar[:open],
375
- high: bar["high"] || bar[:high],
376
- low: bar["low"] || bar[:low],
377
- close: bar["close"] || bar[:close],
378
- volume: bar["volume"] || bar[:volume]
379
- }
380
- end.compact
446
+ build_ohlc_rows(series, max_length)
447
+ end
448
+
449
+ def extract_series(data)
450
+ {
451
+ opens: data[:open] || data["open"] || [],
452
+ highs: data[:high] || data["high"] || [],
453
+ lows: data[:low] || data["low"] || [],
454
+ closes: data[:close] || data["close"] || [],
455
+ volumes: data[:volume] || data["volume"] || []
456
+ }
457
+ end
458
+
459
+ def series_lengths(series)
460
+ [series[:opens].length, series[:highs].length, series[:lows].length, series[:closes].length]
461
+ end
462
+
463
+ def build_ohlc_rows(series, max_length)
464
+ (0...max_length).map do |index|
465
+ {
466
+ open: series[:opens][index] || series[:closes][index] || 0,
467
+ high: series[:highs][index] || series[:closes][index] || 0,
468
+ low: series[:lows][index] || series[:closes][index] || 0,
469
+ close: series[:closes][index] || 0,
470
+ volume: series[:volumes][index] || 0
471
+ }
381
472
  end
473
+ end
382
474
 
383
- []
475
+ def ohlc_from_array(data)
476
+ data.filter_map { |bar| normalize_bar(bar) }
477
+ end
478
+
479
+ def normalize_bar(bar)
480
+ return nil unless bar.is_a?(Hash)
481
+
482
+ {
483
+ open: bar["open"] || bar[:open],
484
+ high: bar["high"] || bar[:high],
485
+ low: bar["low"] || bar[:low],
486
+ close: bar["close"] || bar[:close],
487
+ volume: bar["volume"] || bar[:volume]
488
+ }
384
489
  end
385
490
  end
386
491
  end