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
|
@@ -49,16 +49,14 @@ module DhanHQ
|
|
|
49
49
|
puts " 📋 Rejected candidates:"
|
|
50
50
|
rejected.each do |r|
|
|
51
51
|
puts " ❌ #{r[:symbol]}: Score #{r[:score]}/100 - #{r[:reason]}"
|
|
52
|
-
if r[:details]
|
|
53
|
-
puts " Breakdown: Trend=#{r[:details][:trend]}, RSI=#{r[:details][:rsi]}, MACD=#{r[:details][:macd]}, Structure=#{r[:details][:structure]}, Patterns=#{r[:details][:patterns]}"
|
|
54
|
-
end
|
|
52
|
+
puts " Breakdown: #{format_score_breakdown(r[:details])}" if r[:details]
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
candidates.sort_by { |c| -c[:score] }
|
|
59
57
|
end
|
|
60
58
|
|
|
61
|
-
def scan_by_criteria(
|
|
59
|
+
def scan_by_criteria(_criteria = {})
|
|
62
60
|
# This would typically fetch a list of symbols from an API
|
|
63
61
|
# For now, return empty - would need symbol list source
|
|
64
62
|
[]
|
|
@@ -99,45 +97,15 @@ module DhanHQ
|
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
def calculate_swing_score_details(analysis)
|
|
102
|
-
return
|
|
100
|
+
return empty_score_details if analysis.nil? || analysis.empty?
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
rsi = analysis[:indicators]&.dig(:rsi)
|
|
112
|
-
if rsi
|
|
113
|
-
if rsi.between?(40, 60)
|
|
114
|
-
details[:rsi] = 20
|
|
115
|
-
elsif rsi.between?(30, 70)
|
|
116
|
-
details[:rsi] = 10
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# MACD (0-20 points) - bullish crossover
|
|
121
|
-
macd = analysis[:indicators]&.dig(:macd)
|
|
122
|
-
signal = analysis[:indicators]&.dig(:macd_signal)
|
|
123
|
-
if macd && signal && macd > signal
|
|
124
|
-
details[:macd] = 20
|
|
125
|
-
elsif macd && signal && macd > signal * 0.9
|
|
126
|
-
details[:macd] = 10
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Structure break (0-15 points)
|
|
130
|
-
structure_break = analysis[:structure_break]
|
|
131
|
-
if structure_break && structure_break[:broken] && structure_break[:direction] == :bullish_break
|
|
132
|
-
details[:structure] = 15
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Patterns (0-15 points)
|
|
136
|
-
patterns = analysis[:patterns]&.dig(:candlestick) || []
|
|
137
|
-
bullish_patterns = patterns.count { |p| p.is_a?(Hash) && p[:type].to_s.include?("bullish") }
|
|
138
|
-
details[:patterns] = [bullish_patterns * 5, 15].min
|
|
139
|
-
|
|
140
|
-
details
|
|
102
|
+
{
|
|
103
|
+
trend: trend_points(analysis),
|
|
104
|
+
rsi: rsi_points(analysis),
|
|
105
|
+
macd: macd_points(analysis),
|
|
106
|
+
structure: structure_points(analysis),
|
|
107
|
+
patterns: pattern_points(analysis)
|
|
108
|
+
}
|
|
141
109
|
end
|
|
142
110
|
|
|
143
111
|
def interpret_for_swing(analysis)
|
|
@@ -149,7 +117,7 @@ module DhanHQ
|
|
|
149
117
|
|
|
150
118
|
if trend == :uptrend && rsi && rsi < 70 && structure_break && structure_break[:broken]
|
|
151
119
|
"Strong swing candidate - uptrend with bullish structure break"
|
|
152
|
-
elsif trend == :uptrend && rsi
|
|
120
|
+
elsif trend == :uptrend && rsi&.between?(40, 60)
|
|
153
121
|
"Good swing candidate - healthy uptrend, RSI in good zone"
|
|
154
122
|
else
|
|
155
123
|
"Moderate candidate - review individual factors"
|
|
@@ -159,54 +127,120 @@ module DhanHQ
|
|
|
159
127
|
def convert_to_ohlc(historical_data)
|
|
160
128
|
return [] unless historical_data.is_a?(Hash)
|
|
161
129
|
|
|
162
|
-
|
|
130
|
+
data = extract_data_payload(historical_data)
|
|
131
|
+
return [] unless data
|
|
132
|
+
|
|
133
|
+
return ohlc_from_hash(data) if data.is_a?(Hash)
|
|
134
|
+
return ohlc_from_array(data) if data.is_a?(Array)
|
|
135
|
+
|
|
136
|
+
[]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def format_score_breakdown(details)
|
|
140
|
+
"Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
|
|
141
|
+
"Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def empty_score_details
|
|
145
|
+
{ trend: 0, rsi: 0, macd: 0, structure: 0, patterns: 0 }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def trend_points(analysis)
|
|
149
|
+
trend = analysis[:trend]
|
|
150
|
+
return 0 unless trend && trend[:trend] == :uptrend
|
|
151
|
+
|
|
152
|
+
(trend[:strength] || 0).clamp(0, 30)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def rsi_points(analysis)
|
|
156
|
+
rsi = analysis[:indicators]&.dig(:rsi)
|
|
157
|
+
return 0 unless rsi
|
|
158
|
+
return 20 if rsi.between?(40, 60)
|
|
159
|
+
return 10 if rsi.between?(30, 70)
|
|
160
|
+
|
|
161
|
+
0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def macd_points(analysis)
|
|
165
|
+
macd = analysis[:indicators]&.dig(:macd)
|
|
166
|
+
signal = analysis[:indicators]&.dig(:macd_signal)
|
|
167
|
+
return 0 unless macd && signal
|
|
168
|
+
|
|
169
|
+
return 20 if macd > signal
|
|
170
|
+
return 10 if macd > signal * 0.9
|
|
171
|
+
|
|
172
|
+
0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def structure_points(analysis)
|
|
176
|
+
structure_break = analysis[:structure_break]
|
|
177
|
+
return 0 unless structure_break && structure_break[:broken]
|
|
178
|
+
return 0 unless structure_break[:direction] == :bullish_break
|
|
179
|
+
|
|
180
|
+
15
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def pattern_points(analysis)
|
|
184
|
+
patterns = analysis[:patterns]&.dig(:candlestick) || []
|
|
185
|
+
bullish_patterns = patterns.count { |pattern| pattern.is_a?(Hash) && pattern[:type].to_s.include?("bullish") }
|
|
186
|
+
[bullish_patterns * 5, 15].min
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def extract_data_payload(historical_data)
|
|
163
190
|
outer_result = historical_data[:result] || historical_data["result"]
|
|
164
|
-
return
|
|
191
|
+
return nil unless outer_result.is_a?(Hash)
|
|
165
192
|
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
outer_result[:data] || outer_result["data"]
|
|
194
|
+
end
|
|
168
195
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
highs = data[:high] || data["high"] || []
|
|
173
|
-
lows = data[:low] || data["low"] || []
|
|
174
|
-
closes = data[:close] || data["close"] || []
|
|
175
|
-
volumes = data[:volume] || data["volume"] || []
|
|
176
|
-
|
|
177
|
-
return [] if closes.empty?
|
|
178
|
-
|
|
179
|
-
# Convert parallel arrays to array of hashes
|
|
180
|
-
max_length = [opens.length, highs.length, lows.length, closes.length].max
|
|
181
|
-
ohlc_data = []
|
|
182
|
-
|
|
183
|
-
(0...max_length).each do |i|
|
|
184
|
-
ohlc_data << {
|
|
185
|
-
open: opens[i],
|
|
186
|
-
high: highs[i],
|
|
187
|
-
low: lows[i],
|
|
188
|
-
close: closes[i],
|
|
189
|
-
volume: volumes[i] || 0
|
|
190
|
-
}
|
|
191
|
-
end
|
|
196
|
+
def ohlc_from_hash(data)
|
|
197
|
+
series = extract_series(data)
|
|
198
|
+
return [] if series[:closes].empty?
|
|
192
199
|
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
max_length = series_lengths(series).max
|
|
201
|
+
build_ohlc_rows(series, max_length)
|
|
202
|
+
end
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
def extract_series(data)
|
|
205
|
+
{
|
|
206
|
+
opens: data[:open] || data["open"] || [],
|
|
207
|
+
highs: data[:high] || data["high"] || [],
|
|
208
|
+
lows: data[:low] || data["low"] || [],
|
|
209
|
+
closes: data[:close] || data["close"] || [],
|
|
210
|
+
volumes: data[:volume] || data["volume"] || []
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def series_lengths(series)
|
|
215
|
+
[series[:opens].length, series[:highs].length, series[:lows].length, series[:closes].length]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_ohlc_rows(series, max_length)
|
|
219
|
+
(0...max_length).map do |index|
|
|
220
|
+
{
|
|
221
|
+
open: series[:opens][index],
|
|
222
|
+
high: series[:highs][index],
|
|
223
|
+
low: series[:lows][index],
|
|
224
|
+
close: series[:closes][index],
|
|
225
|
+
volume: series[:volumes][index] || 0
|
|
226
|
+
}
|
|
207
227
|
end
|
|
228
|
+
end
|
|
208
229
|
|
|
209
|
-
|
|
230
|
+
def ohlc_from_array(data)
|
|
231
|
+
data.filter_map { |bar| normalize_bar(bar) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def normalize_bar(bar)
|
|
235
|
+
return nil unless bar.is_a?(Hash)
|
|
236
|
+
|
|
237
|
+
{
|
|
238
|
+
open: bar["open"] || bar[:open],
|
|
239
|
+
high: bar["high"] || bar[:high],
|
|
240
|
+
low: bar["low"] || bar[:low],
|
|
241
|
+
close: bar["close"] || bar[:close],
|
|
242
|
+
volume: bar["volume"] || bar[:volume]
|
|
243
|
+
}
|
|
210
244
|
end
|
|
211
245
|
end
|
|
212
246
|
end
|
|
@@ -21,7 +21,7 @@ module DhanHQ
|
|
|
21
21
|
"type" => "number",
|
|
22
22
|
"minimum" => 0,
|
|
23
23
|
"maximum" => 1,
|
|
24
|
-
"description" => "Confidence in this decision"
|
|
24
|
+
"description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
|
|
25
25
|
},
|
|
26
26
|
"parameters" => {
|
|
27
27
|
"type" => "object",
|
|
@@ -47,7 +47,7 @@ module DhanHQ
|
|
|
47
47
|
"type" => "number",
|
|
48
48
|
"minimum" => 0,
|
|
49
49
|
"maximum" => 1,
|
|
50
|
-
"description" => "Confidence in this decision"
|
|
50
|
+
"description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
|
|
51
51
|
},
|
|
52
52
|
"parameters" => {
|
|
53
53
|
"type" => "object",
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
require_relative "base_service"
|
|
4
4
|
require_relative "../../dhanhq_tools"
|
|
5
5
|
|
|
6
|
-
# Keep backward compatibility - these are still used by the service
|
|
7
|
-
DhanHQDataTools = DhanHQDataTools unless defined?(DhanHQDataTools)
|
|
8
|
-
DhanHQTradingTools = DhanHQTradingTools unless defined?(DhanHQTradingTools)
|
|
9
|
-
|
|
10
6
|
module DhanHQ
|
|
11
7
|
module Services
|
|
12
8
|
# Service for executing data retrieval actions
|
|
@@ -85,9 +81,11 @@ module DhanHQ
|
|
|
85
81
|
end
|
|
86
82
|
|
|
87
83
|
def execute_expired_options_data(params)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
if missing_expired_options_params?(params)
|
|
85
|
+
return error_response("get_expired_options_data",
|
|
86
|
+
"Either symbol or security_id, and expiry_date are required",
|
|
87
|
+
params)
|
|
88
|
+
end
|
|
91
89
|
|
|
92
90
|
DhanHQDataTools.get_expired_options_data(
|
|
93
91
|
symbol: params["symbol"],
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
require_relative "base_service"
|
|
4
4
|
require_relative "../../dhanhq_tools"
|
|
5
5
|
|
|
6
|
-
# Keep backward compatibility
|
|
7
|
-
DhanHQTradingTools = DhanHQTradingTools unless defined?(DhanHQTradingTools)
|
|
8
|
-
|
|
9
6
|
module DhanHQ
|
|
10
7
|
module Services
|
|
11
8
|
# Service for executing trading order actions
|
|
@@ -78,104 +78,187 @@ puts
|
|
|
78
78
|
# Initialize agent
|
|
79
79
|
agent = DhanHQ::Agent.new
|
|
80
80
|
|
|
81
|
-
# Define tools
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
end
|
|
118
|
-
}
|
|
119
|
-
rescue StandardError => e
|
|
120
|
-
{ error: e.message, backtrace: e.backtrace.first(3) }
|
|
121
|
-
end
|
|
122
|
-
end,
|
|
81
|
+
# Define tools using structured Tool classes for better type safety and LLM understanding
|
|
82
|
+
# This provides explicit schemas with descriptions, types, and enums
|
|
83
|
+
|
|
84
|
+
# Swing Scan Tool - Structured definition
|
|
85
|
+
swing_scan_tool = Ollama::Tool.new(
|
|
86
|
+
type: "function",
|
|
87
|
+
function: Ollama::Tool::Function.new(
|
|
88
|
+
name: "swing_scan",
|
|
89
|
+
description: "Scans for swing trading opportunities in EQUITY STOCKS only. " \
|
|
90
|
+
"Use for stocks like RELIANCE, TCS, INFY, HDFC. " \
|
|
91
|
+
"Do NOT use for indices (NIFTY, SENSEX, BANKNIFTY).",
|
|
92
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Stock symbol to scan (e.g., RELIANCE, TCS, INFY). Must be a stock, not an index."
|
|
98
|
+
),
|
|
99
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Exchange segment (default: NSE_EQ)",
|
|
102
|
+
enum: %w[NSE_EQ BSE_EQ]
|
|
103
|
+
),
|
|
104
|
+
min_score: Ollama::Tool::Function::Parameters::Property.new(
|
|
105
|
+
type: "integer",
|
|
106
|
+
description: "Minimum score threshold (default: 40)"
|
|
107
|
+
),
|
|
108
|
+
verbose: Ollama::Tool::Function::Parameters::Property.new(
|
|
109
|
+
type: "boolean",
|
|
110
|
+
description: "Verbose output (default: false)"
|
|
111
|
+
)
|
|
112
|
+
},
|
|
113
|
+
required: %w[symbol]
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
123
117
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
# Options Scan Tool - Structured definition
|
|
119
|
+
options_scan_tool = Ollama::Tool.new(
|
|
120
|
+
type: "function",
|
|
121
|
+
function: Ollama::Tool::Function.new(
|
|
122
|
+
name: "options_scan",
|
|
123
|
+
description: "Scans for intraday options buying opportunities in INDICES only. " \
|
|
124
|
+
"Use for NIFTY, SENSEX, BANKNIFTY. Do NOT use for stocks.",
|
|
125
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY). Must be an index, not a stock.",
|
|
131
|
+
enum: %w[NIFTY SENSEX BANKNIFTY]
|
|
132
|
+
),
|
|
133
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Exchange segment (default: IDX_I)",
|
|
136
|
+
enum: %w[IDX_I]
|
|
137
|
+
),
|
|
138
|
+
min_score: Ollama::Tool::Function::Parameters::Property.new(
|
|
139
|
+
type: "integer",
|
|
140
|
+
description: "Minimum score threshold (default: 40)"
|
|
141
|
+
),
|
|
142
|
+
verbose: Ollama::Tool::Function::Parameters::Property.new(
|
|
143
|
+
type: "boolean",
|
|
144
|
+
description: "Verbose output (default: false)"
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
required: %w[symbol]
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
)
|
|
129
151
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
# Technical Analysis Tool - Structured definition
|
|
153
|
+
technical_analysis_tool = Ollama::Tool.new(
|
|
154
|
+
type: "function",
|
|
155
|
+
function: Ollama::Tool::Function.new(
|
|
156
|
+
name: "technical_analysis",
|
|
157
|
+
description: "Performs full technical analysis including trend, indicators, and patterns. " \
|
|
158
|
+
"Can be used for both stocks and indices.",
|
|
159
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Symbol to analyze (stock or index)"
|
|
165
|
+
),
|
|
166
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Exchange segment (default: NSE_EQ)",
|
|
169
|
+
enum: %w[NSE_EQ BSE_EQ NSE_FNO BSE_FNO IDX_I]
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
required: %w[symbol]
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
177
|
+
# Define tools with structured Tool classes and callables
|
|
178
|
+
tools = {
|
|
179
|
+
"swing_scan" => {
|
|
180
|
+
tool: swing_scan_tool,
|
|
181
|
+
callable: lambda do |symbol:, exchange_segment: "NSE_EQ", min_score: 40, verbose: false|
|
|
182
|
+
# CRITICAL: Only allow equity stocks, not indices
|
|
183
|
+
if %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
|
|
184
|
+
return { error: "#{symbol} is an index, not a stock. Use options_scan for indices." }
|
|
185
|
+
end
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
187
|
+
begin
|
|
188
|
+
candidates = agent.swing_scanner.scan_symbols(
|
|
189
|
+
[symbol.to_s],
|
|
190
|
+
exchange_segment: exchange_segment.to_s,
|
|
191
|
+
min_score: min_score.to_i,
|
|
192
|
+
verbose: verbose
|
|
193
|
+
)
|
|
146
194
|
|
|
147
|
-
if options_setups[:error]
|
|
148
|
-
{ error: options_setups[:error] }
|
|
149
|
-
else
|
|
150
195
|
{
|
|
151
196
|
symbol: symbol,
|
|
152
197
|
exchange_segment: exchange_segment,
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
candidates_found: candidates.length,
|
|
199
|
+
candidates: candidates.map do |c|
|
|
155
200
|
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
recommendation: s[:recommendation]
|
|
201
|
+
symbol: c[:symbol],
|
|
202
|
+
score: c[:score],
|
|
203
|
+
trend: c[:analysis][:trend][:trend],
|
|
204
|
+
recommendation: c[:recommendation]
|
|
161
205
|
}
|
|
162
|
-
end
|
|
206
|
+
end
|
|
163
207
|
}
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
{ error: e.message, backtrace: e.backtrace.first(3) }
|
|
164
210
|
end
|
|
165
|
-
rescue StandardError => e
|
|
166
|
-
{ error: e.message, backtrace: e.backtrace.first(3) }
|
|
167
211
|
end
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
"options_scan" => {
|
|
215
|
+
tool: options_scan_tool,
|
|
216
|
+
callable: lambda do |symbol:, exchange_segment: "IDX_I", min_score: 40, verbose: false|
|
|
217
|
+
# CRITICAL: Only allow indices, not stocks
|
|
218
|
+
unless %w[NIFTY SENSEX BANKNIFTY].include?(symbol.to_s.upcase)
|
|
219
|
+
error_message = "#{symbol} is not an index. Use swing_scan for stocks. " \
|
|
220
|
+
"Options are only available for indices (NIFTY, SENSEX, BANKNIFTY)."
|
|
221
|
+
return { error: error_message }
|
|
222
|
+
end
|
|
173
223
|
|
|
174
|
-
|
|
175
|
-
|
|
224
|
+
begin
|
|
225
|
+
options_setups = agent.options_scanner.scan_for_options_setups(
|
|
226
|
+
symbol.to_s,
|
|
227
|
+
exchange_segment: exchange_segment.to_s,
|
|
228
|
+
min_score: min_score.to_i,
|
|
229
|
+
verbose: verbose
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if options_setups[:error]
|
|
233
|
+
{ error: options_setups[:error] }
|
|
234
|
+
else
|
|
235
|
+
underlying_price = options_setups.dig(:underlying_analysis, :current_price)
|
|
236
|
+
{
|
|
237
|
+
symbol: symbol,
|
|
238
|
+
exchange_segment: exchange_segment,
|
|
239
|
+
underlying_price: underlying_price,
|
|
240
|
+
setups_found: options_setups[:setups]&.length || 0,
|
|
241
|
+
setups: options_setups[:setups]&.map do |s|
|
|
242
|
+
{
|
|
243
|
+
type: s[:type],
|
|
244
|
+
strike: s[:strike],
|
|
245
|
+
score: s[:score],
|
|
246
|
+
iv: s[:iv],
|
|
247
|
+
ltp: s[:ltp],
|
|
248
|
+
recommendation: s[:recommendation]
|
|
249
|
+
}
|
|
250
|
+
end || []
|
|
251
|
+
}
|
|
252
|
+
end
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
{ error: e.message, backtrace: e.backtrace.first(3) }
|
|
255
|
+
end
|
|
176
256
|
end
|
|
257
|
+
},
|
|
177
258
|
|
|
178
|
-
|
|
259
|
+
"technical_analysis" => {
|
|
260
|
+
tool: technical_analysis_tool,
|
|
261
|
+
callable: lambda do |symbol:, exchange_segment: "NSE_EQ"|
|
|
179
262
|
analysis_result = agent.analysis_agent.analyze_symbol(
|
|
180
263
|
symbol: symbol.to_s,
|
|
181
264
|
exchange_segment: exchange_segment.to_s
|
|
@@ -200,7 +283,7 @@ tools = {
|
|
|
200
283
|
rescue StandardError => e
|
|
201
284
|
{ error: e.message, backtrace: e.backtrace.first(3) }
|
|
202
285
|
end
|
|
203
|
-
|
|
286
|
+
}
|
|
204
287
|
}
|
|
205
288
|
|
|
206
289
|
# Get user query and market context
|
|
@@ -254,6 +337,51 @@ executor = Ollama::Agent::Executor.new(
|
|
|
254
337
|
max_steps: 20
|
|
255
338
|
)
|
|
256
339
|
|
|
340
|
+
def tool_messages(messages)
|
|
341
|
+
messages.select { |message| message[:role] == "tool" }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def print_tool_results(messages)
|
|
345
|
+
puts "Tool Results:"
|
|
346
|
+
tool_messages(messages).each do |message|
|
|
347
|
+
print_tool_message(message)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def print_tool_message(message)
|
|
352
|
+
tool_name = message[:name] || "unknown_tool"
|
|
353
|
+
puts "- #{tool_name}"
|
|
354
|
+
puts format_tool_content(message[:content])
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def format_tool_content(content)
|
|
358
|
+
parsed = parse_tool_content(content)
|
|
359
|
+
return parsed if parsed.is_a?(String)
|
|
360
|
+
|
|
361
|
+
JSON.pretty_generate(parsed)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def parse_tool_content(content)
|
|
365
|
+
return content unless content.is_a?(String)
|
|
366
|
+
|
|
367
|
+
JSON.parse(content)
|
|
368
|
+
rescue JSON::ParserError
|
|
369
|
+
content
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def print_llm_summary(result)
|
|
373
|
+
return unless ENV["SHOW_LLM_SUMMARY"] == "true"
|
|
374
|
+
|
|
375
|
+
puts
|
|
376
|
+
puts "LLM Summary (unverified):"
|
|
377
|
+
puts result
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def print_hallucination_warning
|
|
381
|
+
puts "No tool results were produced."
|
|
382
|
+
puts "LLM output suppressed to avoid hallucinated data."
|
|
383
|
+
end
|
|
384
|
+
|
|
257
385
|
# Run the agentic loop
|
|
258
386
|
begin
|
|
259
387
|
puts "🔄 Starting agentic tool-calling loop..."
|
|
@@ -268,7 +396,12 @@ begin
|
|
|
268
396
|
puts "=" * 60
|
|
269
397
|
puts "Agentic Analysis Complete"
|
|
270
398
|
puts "=" * 60
|
|
271
|
-
|
|
399
|
+
if tool_messages(executor.messages).empty?
|
|
400
|
+
print_hallucination_warning
|
|
401
|
+
else
|
|
402
|
+
print_tool_results(executor.messages)
|
|
403
|
+
print_llm_summary(result)
|
|
404
|
+
end
|
|
272
405
|
rescue Ollama::Error => e
|
|
273
406
|
puts "❌ Error: #{e.message}"
|
|
274
407
|
puts e.backtrace.first(5).join("\n") if e.backtrace
|