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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +220 -12
- data/docs/CLOUD.md +29 -0
- data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
- data/docs/FEATURES_ADDED.md +145 -0
- data/docs/HANDLERS_ANALYSIS.md +190 -0
- data/docs/README.md +37 -0
- data/docs/SCHEMA_FIXES.md +147 -0
- data/docs/TEST_UPDATES.md +107 -0
- data/examples/README.md +92 -0
- data/examples/advanced_complex_schemas.rb +6 -3
- data/examples/advanced_multi_step_agent.rb +13 -7
- data/examples/chat_console.rb +143 -0
- data/examples/complete_workflow.rb +14 -4
- data/examples/dhan_console.rb +843 -0
- data/examples/dhanhq/agents/base_agent.rb +0 -2
- data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
- data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
- data/examples/dhanhq/analysis/market_structure.rb +44 -28
- data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
- data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
- data/examples/dhanhq/dhanhq_agent.rb +296 -99
- data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
- data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
- data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
- data/examples/dhanhq/services/data_service.rb +5 -7
- data/examples/dhanhq/services/trading_service.rb +0 -3
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
- data/examples/dhanhq/technical_analysis_runner.rb +216 -162
- data/examples/dhanhq/test_tool_calling.rb +538 -0
- data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
- data/examples/dhanhq_agent.rb +159 -116
- data/examples/dhanhq_tools.rb +1158 -251
- data/examples/multi_step_agent_with_external_data.rb +368 -0
- data/examples/structured_tools.rb +89 -0
- data/examples/test_dhanhq_tool_calling.rb +375 -0
- data/examples/test_tool_calling.rb +160 -0
- data/examples/tool_calling_direct.rb +124 -0
- data/examples/tool_dto_example.rb +94 -0
- data/exe/dhan_console +4 -0
- data/exe/ollama-client +1 -1
- data/lib/ollama/agent/executor.rb +116 -15
- data/lib/ollama/client.rb +118 -55
- data/lib/ollama/config.rb +36 -0
- data/lib/ollama/dto.rb +187 -0
- data/lib/ollama/embeddings.rb +77 -0
- data/lib/ollama/options.rb +104 -0
- data/lib/ollama/response.rb +121 -0
- data/lib/ollama/tool/function/parameters/property.rb +72 -0
- data/lib/ollama/tool/function/parameters.rb +101 -0
- data/lib/ollama/tool/function.rb +78 -0
- data/lib/ollama/tool.rb +60 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +3 -0
- metadata +31 -3
- /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
|
253
|
+
return nil
|
|
148
254
|
end
|
|
149
255
|
|
|
150
256
|
chain = result[:chain] || result["chain"]
|
|
257
|
+
return chain if chain
|
|
151
258
|
|
|
152
|
-
unless
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
276
|
-
|
|
277
|
+
def log_chain_summary(chain, trend, verbose)
|
|
278
|
+
return unless verbose
|
|
277
279
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
434
|
+
return nil unless outer_result.is_a?(Hash)
|
|
335
435
|
|
|
336
|
-
|
|
337
|
-
|
|
436
|
+
outer_result[:data] || outer_result["data"]
|
|
437
|
+
end
|
|
338
438
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|