ollama-client 0.2.5 → 0.2.7

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +336 -91
  4. data/RELEASE_NOTES_v0.2.6.md +41 -0
  5. data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
  6. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  7. data/docs/FEATURES_ADDED.md +12 -1
  8. data/docs/GETTING_STARTED.md +361 -0
  9. data/docs/INTEGRATION_TESTING.md +170 -0
  10. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  11. data/docs/PERSONAS.md +383 -0
  12. data/docs/QUICK_START.md +195 -0
  13. data/docs/TESTING.md +392 -170
  14. data/docs/TEST_CHECKLIST.md +450 -0
  15. data/examples/README.md +62 -63
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/mcp_executor.rb +39 -0
  19. data/examples/mcp_http_executor.rb +45 -0
  20. data/examples/tool_calling_parsing.rb +59 -0
  21. data/examples/tool_dto_example.rb +0 -0
  22. data/exe/ollama-client +128 -1
  23. data/lib/ollama/agent/planner.rb +7 -2
  24. data/lib/ollama/chat_session.rb +101 -0
  25. data/lib/ollama/client.rb +41 -35
  26. data/lib/ollama/config.rb +9 -4
  27. data/lib/ollama/document_loader.rb +1 -1
  28. data/lib/ollama/embeddings.rb +61 -28
  29. data/lib/ollama/errors.rb +1 -0
  30. data/lib/ollama/mcp/http_client.rb +149 -0
  31. data/lib/ollama/mcp/stdio_client.rb +146 -0
  32. data/lib/ollama/mcp/tools_bridge.rb +72 -0
  33. data/lib/ollama/mcp.rb +31 -0
  34. data/lib/ollama/options.rb +3 -1
  35. data/lib/ollama/personas.rb +287 -0
  36. data/lib/ollama/version.rb +1 -1
  37. data/lib/ollama_client.rb +17 -5
  38. metadata +22 -48
  39. data/examples/advanced_complex_schemas.rb +0 -366
  40. data/examples/advanced_edge_cases.rb +0 -241
  41. data/examples/advanced_error_handling.rb +0 -200
  42. data/examples/advanced_multi_step_agent.rb +0 -341
  43. data/examples/advanced_performance_testing.rb +0 -186
  44. data/examples/chat_console.rb +0 -143
  45. data/examples/complete_workflow.rb +0 -245
  46. data/examples/dhan_console.rb +0 -843
  47. data/examples/dhanhq/README.md +0 -236
  48. data/examples/dhanhq/agents/base_agent.rb +0 -74
  49. data/examples/dhanhq/agents/data_agent.rb +0 -66
  50. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  51. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  52. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  53. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  54. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  55. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  56. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  57. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  58. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  59. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  60. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  61. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  62. data/examples/dhanhq/services/base_service.rb +0 -46
  63. data/examples/dhanhq/services/data_service.rb +0 -118
  64. data/examples/dhanhq/services/trading_service.rb +0 -59
  65. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  66. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  67. data/examples/dhanhq/test_tool_calling.rb +0 -538
  68. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  69. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  70. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  71. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  72. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  73. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  74. data/examples/dhanhq_agent.rb +0 -964
  75. data/examples/dhanhq_tools.rb +0 -1663
  76. data/examples/multi_step_agent_with_external_data.rb +0 -368
  77. data/examples/structured_outputs_chat.rb +0 -72
  78. data/examples/structured_tools.rb +0 -89
  79. data/examples/test_dhanhq_tool_calling.rb +0 -375
  80. data/examples/test_tool_calling.rb +0 -160
  81. data/examples/tool_calling_direct.rb +0 -124
  82. data/examples/tool_calling_pattern.rb +0 -269
  83. data/exe/dhan_console +0 -4
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Indicators
5
- # Technical analysis indicators
6
- class TechnicalIndicators
7
- # Simple Moving Average
8
- def self.sma(prices, period)
9
- return [] if prices.nil? || prices.empty? || period < 1
10
-
11
- prices.each_cons(period).map { |window| window.sum.to_f / period }
12
- end
13
-
14
- # Exponential Moving Average
15
- def self.ema(prices, period)
16
- return [] if prices.nil? || prices.empty? || period < 1
17
-
18
- multiplier = 2.0 / (period + 1)
19
- ema_values = []
20
- ema_values << prices.first.to_f
21
-
22
- prices[1..].each do |price|
23
- ema_values << ((price.to_f - ema_values.last) * multiplier + ema_values.last)
24
- end
25
-
26
- ema_values
27
- end
28
-
29
- # Relative Strength Index
30
- def self.rsi(prices, period = 14)
31
- return [] if prices.nil? || prices.length < period + 1
32
-
33
- gains = []
34
- losses = []
35
-
36
- prices.each_cons(2) do |prev, curr|
37
- change = curr - prev
38
- gains << [change, 0].max
39
- losses << (change.negative? ? -change : 0)
40
- end
41
-
42
- rsi_values = []
43
- avg_gain = gains.first(period).sum.to_f / period
44
- avg_loss = losses.first(period).sum.to_f / period
45
-
46
- rsi_values << calculate_rsi_value(avg_gain, avg_loss)
47
-
48
- (period...gains.length).each do |i|
49
- avg_gain = ((avg_gain * (period - 1)) + gains[i]) / period
50
- avg_loss = ((avg_loss * (period - 1)) + losses[i]) / period
51
- rsi_values << calculate_rsi_value(avg_gain, avg_loss)
52
- end
53
-
54
- rsi_values
55
- end
56
-
57
- # MACD (Moving Average Convergence Divergence)
58
- def self.macd(prices, fast_period = 12, slow_period = 26, signal_period = 9)
59
- return { macd: [], signal: [], histogram: [] } if prices.nil? || prices.empty?
60
-
61
- fast_ema = ema(prices, fast_period)
62
- slow_ema = ema(prices, slow_period)
63
-
64
- min_length = [fast_ema.length, slow_ema.length].min
65
- macd_line = fast_ema.last(min_length).zip(slow_ema.last(min_length)).map { |f, s| f - s }
66
-
67
- signal_line = ema(macd_line, signal_period)
68
- histogram = macd_line.last(signal_line.length).zip(signal_line).map { |m, s| m - s }
69
-
70
- {
71
- macd: macd_line,
72
- signal: signal_line,
73
- histogram: histogram
74
- }
75
- end
76
-
77
- # Bollinger Bands
78
- def self.bollinger_bands(prices, period = 20, std_dev = 2)
79
- return { upper: [], middle: [], lower: [] } if prices.nil? || prices.empty?
80
-
81
- sma_values = sma(prices, period)
82
- bands = { upper: [], middle: sma_values, lower: [] }
83
-
84
- prices.each_cons(period).with_index do |window, idx|
85
- mean = sma_values[idx]
86
- variance = window.map { |p| (p - mean)**2 }.sum / period
87
- std = Math.sqrt(variance)
88
-
89
- bands[:upper] << mean + (std_dev * std)
90
- bands[:lower] << mean - (std_dev * std)
91
- end
92
-
93
- bands
94
- end
95
-
96
- # Average True Range
97
- def self.atr(highs, lows, closes, period = 14)
98
- return [] if highs.nil? || lows.nil? || closes.nil?
99
- return [] if [highs.length, lows.length, closes.length].min < 2
100
-
101
- true_ranges = []
102
- (1...highs.length).each do |i|
103
- tr1 = highs[i] - lows[i]
104
- tr2 = (highs[i] - closes[i - 1]).abs
105
- tr3 = (lows[i] - closes[i - 1]).abs
106
- true_ranges << [tr1, tr2, tr3].max
107
- end
108
-
109
- return [] if true_ranges.length < period
110
-
111
- atr_values = []
112
- atr_values << true_ranges.first(period).sum.to_f / period
113
-
114
- (period...true_ranges.length).each do |i|
115
- atr = ((atr_values.last * (period - 1)) + true_ranges[i]) / period
116
- atr_values << atr
117
- end
118
-
119
- atr_values
120
- end
121
-
122
- # Support and Resistance levels
123
- def self.support_resistance(highs, lows, closes, lookback = 20)
124
- return { support: [], resistance: [] } if highs.nil? || lows.nil? || closes.nil?
125
-
126
- support_levels = []
127
- resistance_levels = []
128
-
129
- (lookback...highs.length).each do |i|
130
- window_highs = highs[(i - lookback)..i]
131
- window_lows = lows[(i - lookback)..i]
132
-
133
- local_high = window_highs.max
134
- local_low = window_lows.min
135
-
136
- # Resistance: price touched high multiple times
137
- if window_highs.count(local_high) >= 2
138
- resistance_levels << { price: local_high, strength: window_highs.count(local_high) }
139
- end
140
-
141
- # Support: price touched low multiple times
142
- if window_lows.count(local_low) >= 2
143
- support_levels << { price: local_low, strength: window_lows.count(local_low) }
144
- end
145
- end
146
-
147
- { support: support_levels.uniq { |s| s[:price] }, resistance: resistance_levels.uniq { |r| r[:price] } }
148
- end
149
-
150
- def self.calculate_rsi_value(avg_gain, avg_loss)
151
- return 100 if avg_loss.zero?
152
-
153
- rs = avg_gain / avg_loss
154
- 100 - (100 / (1 + rs))
155
- end
156
- end
157
- end
158
- end
@@ -1,492 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../analysis/trend_analyzer"
4
- require_relative "../services/data_service"
5
-
6
- module DhanHQ
7
- module Scanners
8
- # Scanner for intraday options buying opportunities
9
- class IntradayOptionsScanner
10
- def initialize
11
- @data_service = Services::DataService.new
12
- end
13
-
14
- def scan_for_options_setups(underlying_symbol, exchange_segment: "IDX_I", min_score: 50, verbose: false)
15
- puts " 🔍 Analyzing underlying: #{underlying_symbol} (#{exchange_segment})" if verbose
16
-
17
- # Analyze underlying
18
- underlying_analysis = analyze_underlying(underlying_symbol, exchange_segment)
19
- if underlying_analysis[:error]
20
- return error_result("Failed to analyze underlying: #{underlying_analysis[:error]}", verbose)
21
- end
22
-
23
- puts " ✅ Underlying analysis complete" if verbose
24
-
25
- # Get option chain - first get expiry list, then fetch chain for first expiry
26
- expiry_list_result = get_option_chain(underlying_symbol, exchange_segment)
27
- if expiry_list_result[:error]
28
- return error_result("Failed to get expiry list: #{expiry_list_result[:error]}", verbose)
29
- end
30
-
31
- expiries = extract_expiries(expiry_list_result)
32
- return error_result("No expiries found in option chain", verbose) unless expiries
33
-
34
- # Get chain for first expiry (next expiry)
35
- next_expiry = expiries.first
36
- puts " ✅ Found #{expiries.length} expiries, fetching chain for: #{next_expiry}" if verbose
37
-
38
- option_chain = get_option_chain(underlying_symbol, exchange_segment, expiry: next_expiry)
39
- if option_chain[:error]
40
- return error_result("Failed to get option chain for expiry #{next_expiry}: #{option_chain[:error]}", verbose)
41
- end
42
-
43
- puts " ✅ Option chain retrieved for expiry: #{next_expiry}" if verbose
44
-
45
- # Find best options setups
46
- setups = find_options_setups(underlying_analysis[:analysis], option_chain, min_score: min_score,
47
- verbose: verbose)
48
-
49
- {
50
- underlying: underlying_symbol,
51
- underlying_analysis: underlying_analysis[:analysis],
52
- setups: setups
53
- }
54
- end
55
-
56
- private
57
-
58
- def analyze_underlying(symbol, exchange_segment)
59
- result = @data_service.execute(
60
- action: "get_historical_data",
61
- params: {
62
- "symbol" => symbol,
63
- "exchange_segment" => exchange_segment,
64
- "from_date" => (Date.today - 30).strftime("%Y-%m-%d"),
65
- "to_date" => Date.today.strftime("%Y-%m-%d"),
66
- "interval" => "5" # 5-minute for intraday
67
- }
68
- )
69
-
70
- return { error: "Failed to fetch data: #{result[:error] || 'Unknown error'}" } if result[:error]
71
- return { error: "No result data returned" } unless result[:result]
72
-
73
- ohlc_data = convert_to_ohlc(result)
74
- return { error: "Failed to convert data to OHLC format" } if ohlc_data.nil? || ohlc_data.empty?
75
-
76
- analysis = Analysis::TrendAnalyzer.analyze(ohlc_data)
77
- return { error: "Analysis returned empty result" } if analysis.nil? || analysis.empty?
78
-
79
- {
80
- symbol: symbol,
81
- analysis: analysis
82
- }
83
- end
84
-
85
- def get_option_chain(symbol, exchange_segment, expiry: nil)
86
- @data_service.execute(
87
- action: "get_option_chain",
88
- params: {
89
- "symbol" => symbol,
90
- "exchange_segment" => exchange_segment,
91
- "expiry" => expiry
92
- }
93
- )
94
- end
95
-
96
- def find_options_setups(analysis, option_chain, min_score: 50, verbose: false)
97
- tracking = {
98
- setups: [],
99
- rejected: [],
100
- strikes_evaluated: 0,
101
- strikes_within_range: 0
102
- }
103
-
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
- )
132
- end
133
-
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')}"
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
248
-
249
- def extract_option_chain(option_chain, verbose)
250
- result = option_chain[:result] || option_chain["result"]
251
- unless result
252
- puts " ⚠️ Option chain data not available or invalid (no result)" if verbose
253
- return nil
254
- end
255
-
256
- chain = result[:chain] || result["chain"]
257
- return chain if chain
258
-
259
- return nil unless verbose
260
-
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
266
-
267
- def build_analysis_context(analysis)
268
- return { current_price: nil, trend: nil, relative_strength_index: nil } unless analysis
269
-
270
- {
271
- current_price: analysis[:current_price],
272
- trend: analysis[:trend]&.dig(:trend),
273
- relative_strength_index: analysis[:indicators]&.dig(:rsi)
274
- }
275
- end
276
-
277
- def log_chain_summary(chain, trend, verbose)
278
- return unless verbose
279
-
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
287
-
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
- )
310
- end
311
-
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
341
- end
342
-
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)
395
- end
396
- end
397
-
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"
402
- end
403
- end
404
-
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
418
- end
419
-
420
- def convert_to_ohlc(historical_data)
421
- return [] unless historical_data.is_a?(Hash)
422
-
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)
433
- outer_result = historical_data[:result] || historical_data["result"]
434
- return nil unless outer_result.is_a?(Hash)
435
-
436
- outer_result[:data] || outer_result["data"]
437
- end
438
-
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?
445
-
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
- }
472
- end
473
- end
474
-
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
- }
489
- end
490
- end
491
- end
492
- end