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
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# DhanHQ Tool Calling Test
|
|
5
|
+
# Dedicated test file for tool calling with DhanHQ tools
|
|
6
|
+
# Uses Executor + Structured Tool Classes
|
|
7
|
+
|
|
8
|
+
require_relative "../../lib/ollama_client"
|
|
9
|
+
require_relative "../dhanhq_tools"
|
|
10
|
+
|
|
11
|
+
puts "\n=== DHANHQ TOOL CALLING TEST ===\n"
|
|
12
|
+
|
|
13
|
+
# Configure DhanHQ
|
|
14
|
+
begin
|
|
15
|
+
DhanHQ.configure_with_env
|
|
16
|
+
puts "✅ DhanHQ configured"
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
puts "⚠️ DhanHQ configuration error: #{e.message}"
|
|
19
|
+
puts " Make sure CLIENT_ID and ACCESS_TOKEN are set in ENV"
|
|
20
|
+
puts " Continuing with test (may fail on actual API calls)..."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create client
|
|
24
|
+
config = Ollama::Config.new
|
|
25
|
+
config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
|
|
26
|
+
config.temperature = 0.2
|
|
27
|
+
config.timeout = 60
|
|
28
|
+
client = Ollama::Client.new(config: config)
|
|
29
|
+
|
|
30
|
+
def find_strike_key(strike_keys, strike_value)
|
|
31
|
+
strike_keys.find { |key| (key.to_s.to_f - strike_value).abs < 0.01 }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def log_missing_strike_key(strike_value, strike_keys)
|
|
35
|
+
puts "⚠️ Could not find key for strike ₹#{strike_value}"
|
|
36
|
+
puts " Available keys sample: #{strike_keys.first(3).inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def print_option_leg(heading, data, label)
|
|
40
|
+
puts heading
|
|
41
|
+
if data && !data.empty?
|
|
42
|
+
puts " LTP: ₹#{option_value(data, :ltp)}"
|
|
43
|
+
puts " IV: #{option_value(data, :iv)}%"
|
|
44
|
+
puts " OI: #{option_value(data, :oi)}"
|
|
45
|
+
puts " Volume: #{option_value(data, :volume)}"
|
|
46
|
+
puts " Delta: #{option_value(data, :delta)}"
|
|
47
|
+
puts " Gamma: #{option_value(data, :gamma)}"
|
|
48
|
+
puts " Theta: #{option_value(data, :theta)}"
|
|
49
|
+
puts " Vega: #{option_value(data, :vega)}"
|
|
50
|
+
else
|
|
51
|
+
puts " No #{label} data available"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def option_value(data, key)
|
|
56
|
+
return "N/A" unless data
|
|
57
|
+
|
|
58
|
+
data[key] || data[key.to_s] || "N/A"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def print_strike_summary(strike_value, strike_keys, chain)
|
|
62
|
+
actual_key = find_strike_key(strike_keys, strike_value)
|
|
63
|
+
unless actual_key
|
|
64
|
+
log_missing_strike_key(strike_value, strike_keys)
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
strike_data = chain[actual_key]
|
|
69
|
+
unless strike_data
|
|
70
|
+
puts "⚠️ No data found for strike ₹#{strike_value} (key: #{actual_key.inspect})"
|
|
71
|
+
puts " Strike data type: #{strike_data.class}" if strike_data
|
|
72
|
+
puts " Strike data: #{strike_data.inspect}" if strike_data
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts "=" * 60
|
|
77
|
+
puts "Strike: ₹#{strike_value} (key: #{actual_key.inspect})"
|
|
78
|
+
puts "-" * 60
|
|
79
|
+
|
|
80
|
+
# Extract CALL and PUT data (DhanHQ uses "ce" for CALL and "pe" for PUT)
|
|
81
|
+
call_data = strike_data[:ce] || strike_data["ce"] || {}
|
|
82
|
+
put_data = strike_data[:pe] || strike_data["pe"] || {}
|
|
83
|
+
|
|
84
|
+
print_option_leg("CALL Options (CE):", call_data, "CALL")
|
|
85
|
+
print_option_leg("\nPUT Options (PE):", put_data, "PUT")
|
|
86
|
+
puts
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Define DhanHQ tools using structured Tool classes
|
|
90
|
+
puts "\n--- Defining Tools ---"
|
|
91
|
+
|
|
92
|
+
market_quote_tool = Ollama::Tool.new(
|
|
93
|
+
type: "function",
|
|
94
|
+
function: Ollama::Tool::Function.new(
|
|
95
|
+
name: "get_market_quote",
|
|
96
|
+
description: "Get market quote for a symbol. Returns OHLC, depth, volume, and other market data. " \
|
|
97
|
+
"Finds instrument automatically using exchange_segment and symbol.",
|
|
98
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Stock or index symbol (e.g., RELIANCE, NIFTY)"
|
|
104
|
+
),
|
|
105
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
106
|
+
type: "string",
|
|
107
|
+
description: "Exchange segment",
|
|
108
|
+
enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
required: %w[symbol exchange_segment]
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
live_ltp_tool = Ollama::Tool.new(
|
|
117
|
+
type: "function",
|
|
118
|
+
function: Ollama::Tool::Function.new(
|
|
119
|
+
name: "get_live_ltp",
|
|
120
|
+
description: "Get live last traded price (LTP) for a symbol. Fast API for current price. " \
|
|
121
|
+
"Finds instrument automatically using exchange_segment and symbol.",
|
|
122
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Stock or index symbol"
|
|
128
|
+
),
|
|
129
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Exchange segment",
|
|
132
|
+
enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
required: %w[symbol exchange_segment]
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Option Chain Tool (for indices: NIFTY, SENSEX, BANKNIFTY)
|
|
141
|
+
option_chain_tool = Ollama::Tool.new(
|
|
142
|
+
type: "function",
|
|
143
|
+
function: Ollama::Tool::Function.new(
|
|
144
|
+
name: "get_option_chain",
|
|
145
|
+
description: "Get option chain for an index (NIFTY, SENSEX, BANKNIFTY). " \
|
|
146
|
+
"Returns available expiries and option chain data with strikes, Greeks, OI, and IV.",
|
|
147
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY)",
|
|
153
|
+
enum: %w[NIFTY SENSEX BANKNIFTY]
|
|
154
|
+
),
|
|
155
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Exchange segment (must be IDX_I for indices)",
|
|
158
|
+
enum: %w[IDX_I]
|
|
159
|
+
),
|
|
160
|
+
expiry: Ollama::Tool::Function::Parameters::Property.new(
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Optional expiry date (YYYY-MM-DD format). If not provided, returns available expiries list."
|
|
163
|
+
)
|
|
164
|
+
},
|
|
165
|
+
required: %w[symbol exchange_segment]
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
puts "✅ Tools defined: get_market_quote, get_live_ltp, get_option_chain"
|
|
171
|
+
|
|
172
|
+
# Define tools with structured Tool classes and callables
|
|
173
|
+
tools = {
|
|
174
|
+
"get_market_quote" => {
|
|
175
|
+
tool: market_quote_tool,
|
|
176
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
177
|
+
puts " 🔧 Executing: get_market_quote(#{symbol}, #{exchange_segment})"
|
|
178
|
+
begin
|
|
179
|
+
result = DhanHQDataTools.get_market_quote(
|
|
180
|
+
symbol: symbol.to_s,
|
|
181
|
+
exchange_segment: exchange_segment.to_s
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if result[:error]
|
|
185
|
+
puts " ❌ Error: #{result[:error]}"
|
|
186
|
+
{ error: result[:error] }
|
|
187
|
+
else
|
|
188
|
+
quote = result[:result][:quote]
|
|
189
|
+
response = {
|
|
190
|
+
symbol: symbol,
|
|
191
|
+
exchange_segment: exchange_segment,
|
|
192
|
+
last_price: quote[:last_price],
|
|
193
|
+
volume: quote[:volume],
|
|
194
|
+
ohlc: quote[:ohlc],
|
|
195
|
+
change_percent: quote[:net_change]
|
|
196
|
+
}
|
|
197
|
+
puts " ✅ Success: LTP=#{quote[:last_price]}, Volume=#{quote[:volume]}"
|
|
198
|
+
response
|
|
199
|
+
end
|
|
200
|
+
rescue StandardError => e
|
|
201
|
+
puts " ❌ Exception: #{e.message}"
|
|
202
|
+
{ error: e.message }
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
"get_live_ltp" => {
|
|
208
|
+
tool: live_ltp_tool,
|
|
209
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
210
|
+
puts " 🔧 Executing: get_live_ltp(#{symbol}, #{exchange_segment})"
|
|
211
|
+
begin
|
|
212
|
+
# Add rate limiting delay for MarketFeed APIs
|
|
213
|
+
sleep(1.2) if defined?(DhanHQDataTools) && DhanHQDataTools.respond_to?(:rate_limit_marketfeed)
|
|
214
|
+
DhanHQDataTools.rate_limit_marketfeed if DhanHQDataTools.respond_to?(:rate_limit_marketfeed)
|
|
215
|
+
|
|
216
|
+
result = DhanHQDataTools.get_live_ltp(
|
|
217
|
+
symbol: symbol.to_s,
|
|
218
|
+
exchange_segment: exchange_segment.to_s
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if result[:error]
|
|
222
|
+
puts " ❌ Error: #{result[:error]}"
|
|
223
|
+
{ error: result[:error] }
|
|
224
|
+
else
|
|
225
|
+
response = {
|
|
226
|
+
symbol: symbol,
|
|
227
|
+
exchange_segment: exchange_segment,
|
|
228
|
+
ltp: result[:result][:ltp],
|
|
229
|
+
timestamp: result[:result][:timestamp]
|
|
230
|
+
}
|
|
231
|
+
puts " ✅ Success: LTP=#{result[:result][:ltp]}"
|
|
232
|
+
response
|
|
233
|
+
end
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
puts " ❌ Exception: #{e.message}"
|
|
236
|
+
{ error: e.message }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
"get_option_chain" => {
|
|
242
|
+
tool: option_chain_tool,
|
|
243
|
+
callable: lambda do |symbol:, exchange_segment:, expiry: nil|
|
|
244
|
+
# Normalize empty string to nil (LLM might pass "" when expiry is optional)
|
|
245
|
+
expiry = nil if expiry.is_a?(String) && expiry.empty?
|
|
246
|
+
puts " 🔧 Executing: get_option_chain(#{symbol}, #{exchange_segment}, expiry=#{expiry || 'nil'})"
|
|
247
|
+
begin
|
|
248
|
+
result = DhanHQDataTools.get_option_chain(
|
|
249
|
+
symbol: symbol.to_s,
|
|
250
|
+
exchange_segment: exchange_segment.to_s,
|
|
251
|
+
expiry: expiry
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if result[:error]
|
|
255
|
+
puts " ❌ Error: #{result[:error]}"
|
|
256
|
+
{ error: result[:error] }
|
|
257
|
+
elsif result[:result] && result[:result][:expiries]
|
|
258
|
+
puts " ✅ Success: #{result[:result][:count]} expiries available"
|
|
259
|
+
{
|
|
260
|
+
symbol: symbol,
|
|
261
|
+
expiries_available: result[:result][:expiries],
|
|
262
|
+
count: result[:result][:count]
|
|
263
|
+
}
|
|
264
|
+
elsif result[:result] && result[:result][:chain]
|
|
265
|
+
chain = result[:result][:chain]
|
|
266
|
+
strikes = chain.is_a?(Hash) ? chain.keys.sort_by(&:to_f) : []
|
|
267
|
+
puts " ✅ Success: #{strikes.length} strikes for expiry #{result[:result][:expiry]}"
|
|
268
|
+
{
|
|
269
|
+
symbol: symbol,
|
|
270
|
+
expiry: result[:result][:expiry],
|
|
271
|
+
underlying_price: result[:result][:underlying_last_price],
|
|
272
|
+
strikes_count: strikes.length,
|
|
273
|
+
sample_strikes: strikes.first(5)
|
|
274
|
+
}
|
|
275
|
+
else
|
|
276
|
+
{ error: "Unexpected response format" }
|
|
277
|
+
end
|
|
278
|
+
rescue StandardError => e
|
|
279
|
+
puts " ❌ Exception: #{e.message}"
|
|
280
|
+
{ error: e.message }
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
puts "\n--- Test 1: Single Tool Call ---"
|
|
287
|
+
puts "Request: Get market quote for RELIANCE\n"
|
|
288
|
+
|
|
289
|
+
executor1 = Ollama::Agent::Executor.new(
|
|
290
|
+
client,
|
|
291
|
+
tools: { "get_market_quote" => tools["get_market_quote"] },
|
|
292
|
+
max_steps: 5
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
begin
|
|
296
|
+
result1 = executor1.run(
|
|
297
|
+
system: "You are a market data assistant. Use the get_market_quote tool to get market data.",
|
|
298
|
+
user: "Get the market quote for RELIANCE stock on NSE"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
puts "\n✅ Result:"
|
|
302
|
+
puts result1
|
|
303
|
+
rescue Ollama::Error => e
|
|
304
|
+
puts "\n❌ Error: #{e.message}"
|
|
305
|
+
rescue StandardError => e
|
|
306
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
307
|
+
puts e.backtrace.first(3).join("\n")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
puts "\n" + ("=" * 60)
|
|
311
|
+
puts "--- Test 2: Multiple Tools (LLM Chooses) ---"
|
|
312
|
+
puts "Request: Get RELIANCE quote, NIFTY price, and SENSEX option chain\n"
|
|
313
|
+
|
|
314
|
+
executor2 = Ollama::Agent::Executor.new(
|
|
315
|
+
client,
|
|
316
|
+
tools: tools,
|
|
317
|
+
max_steps: 10
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
begin
|
|
321
|
+
result2 = executor2.run(
|
|
322
|
+
system: "You are a market data assistant. Use the available tools to get market data. " \
|
|
323
|
+
"You can call multiple tools in sequence. When you have the data, summarize it clearly. " \
|
|
324
|
+
"For option chains, use get_option_chain with symbol='SENSEX' and exchange_segment='IDX_I'.",
|
|
325
|
+
user: "Get market quote for RELIANCE stock, check NIFTY's current price, and get SENSEX option chain"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
puts "\n✅ Result:"
|
|
329
|
+
puts result2
|
|
330
|
+
rescue Ollama::Error => e
|
|
331
|
+
puts "\n❌ Error: #{e.message}"
|
|
332
|
+
rescue StandardError => e
|
|
333
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
334
|
+
puts e.backtrace.first(3).join("\n")
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
puts "\n" + ("=" * 60)
|
|
338
|
+
puts "--- Test 3: Option Chain (SENSEX) - Expiry List ---"
|
|
339
|
+
puts "Request: Get SENSEX option chain expiry list\n"
|
|
340
|
+
|
|
341
|
+
executor3 = Ollama::Agent::Executor.new(
|
|
342
|
+
client,
|
|
343
|
+
tools: { "get_option_chain" => tools["get_option_chain"] },
|
|
344
|
+
max_steps: 5
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
begin
|
|
348
|
+
result3 = executor3.run(
|
|
349
|
+
system: "You are a market data assistant. Use the get_option_chain tool to get option chain data for indices. " \
|
|
350
|
+
"When no expiry is specified, it returns the list of available expiries.",
|
|
351
|
+
user: "Get the option chain for SENSEX index"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
puts "\n✅ Result:"
|
|
355
|
+
puts result3
|
|
356
|
+
rescue Ollama::Error => e
|
|
357
|
+
puts "\n❌ Error: #{e.message}"
|
|
358
|
+
rescue StandardError => e
|
|
359
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
360
|
+
puts e.backtrace.first(3).join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
puts "\n" + ("=" * 60)
|
|
364
|
+
puts "--- Test 3b: Option Chain (SENSEX) - Full Chain with Strikes ---"
|
|
365
|
+
puts "Request: Get SENSEX option chain for specific expiry\n"
|
|
366
|
+
|
|
367
|
+
begin
|
|
368
|
+
# First get the expiry list to use a valid expiry
|
|
369
|
+
expiry_result = DhanHQDataTools.get_option_chain(
|
|
370
|
+
symbol: "SENSEX",
|
|
371
|
+
exchange_segment: "IDX_I"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if expiry_result[:result] && expiry_result[:result][:expiries] && !expiry_result[:result][:expiries].empty?
|
|
375
|
+
first_expiry = expiry_result[:result][:expiries].first
|
|
376
|
+
puts "Using expiry: #{first_expiry}\n"
|
|
377
|
+
|
|
378
|
+
# Call directly to avoid LLM date confusion
|
|
379
|
+
chain_result = DhanHQDataTools.get_option_chain(
|
|
380
|
+
symbol: "SENSEX",
|
|
381
|
+
exchange_segment: "IDX_I",
|
|
382
|
+
expiry: first_expiry
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if chain_result[:error]
|
|
386
|
+
puts "❌ Error: #{chain_result[:error]}"
|
|
387
|
+
elsif chain_result[:result] && chain_result[:result][:chain]
|
|
388
|
+
chain = chain_result[:result][:chain]
|
|
389
|
+
underlying_price = chain_result[:result][:underlying_last_price]
|
|
390
|
+
strikes = if chain.is_a?(Hash)
|
|
391
|
+
chain.keys.sort_by { |k| k.to_s.to_f }
|
|
392
|
+
else
|
|
393
|
+
[]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
puts "✅ Option chain retrieved successfully"
|
|
397
|
+
puts " Underlying Price: ₹#{underlying_price}"
|
|
398
|
+
puts " Expiry: #{chain_result[:result][:expiry]}"
|
|
399
|
+
puts " Total Strikes: #{strikes.length}"
|
|
400
|
+
puts " Strike Range: ₹#{strikes.first} to ₹#{strikes.last}" unless strikes.empty?
|
|
401
|
+
puts " Sample strikes: #{strikes.first(5).join(', ')}" unless strikes.empty?
|
|
402
|
+
else
|
|
403
|
+
puts "⚠️ Unexpected response format"
|
|
404
|
+
end
|
|
405
|
+
else
|
|
406
|
+
puts "⚠️ Could not get expiry list to test full chain"
|
|
407
|
+
end
|
|
408
|
+
rescue StandardError => e
|
|
409
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
410
|
+
puts e.backtrace.first(3).join("\n")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
puts "\n" + ("=" * 60)
|
|
414
|
+
puts "--- Test 4: ATM and ATM+1 Strikes (CALL & PUT) ---"
|
|
415
|
+
puts "Request: Get SENSEX option chain and extract ATM, ATM+1 for CALL and PUT\n"
|
|
416
|
+
|
|
417
|
+
begin
|
|
418
|
+
# Get expiry list first
|
|
419
|
+
expiry_result = DhanHQDataTools.get_option_chain(
|
|
420
|
+
symbol: "SENSEX",
|
|
421
|
+
exchange_segment: "IDX_I"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
expiries = expiry_result.dig(:result, :expiries)
|
|
425
|
+
if expiry_result[:error] || !expiries.is_a?(Array) || expiries.empty?
|
|
426
|
+
error_message = expiry_result[:error] || "No expiries found"
|
|
427
|
+
puts "❌ Error: Could not get expiry list - #{error_message}"
|
|
428
|
+
else
|
|
429
|
+
first_expiry = expiries.first
|
|
430
|
+
puts "Using expiry: #{first_expiry}\n"
|
|
431
|
+
|
|
432
|
+
# Get full option chain for this expiry
|
|
433
|
+
chain_result = DhanHQDataTools.get_option_chain(
|
|
434
|
+
symbol: "SENSEX",
|
|
435
|
+
exchange_segment: "IDX_I",
|
|
436
|
+
expiry: first_expiry
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if chain_result[:error]
|
|
440
|
+
puts "❌ Error getting option chain: #{chain_result[:error]}"
|
|
441
|
+
elsif chain_result[:result] && chain_result[:result][:chain]
|
|
442
|
+
underlying_price = chain_result[:result][:underlying_last_price].to_f
|
|
443
|
+
chain = chain_result[:result][:chain]
|
|
444
|
+
|
|
445
|
+
puts "Underlying Price (SENSEX): ₹#{underlying_price}\n"
|
|
446
|
+
|
|
447
|
+
# Extract all strikes and sort them
|
|
448
|
+
# Chain keys are typically strings with decimal precision (e.g., "83600.000000")
|
|
449
|
+
strike_keys = if chain.is_a?(Hash)
|
|
450
|
+
chain.keys.sort_by { |k| k.to_s.to_f }
|
|
451
|
+
else
|
|
452
|
+
[]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
strikes = strike_keys.map { |k| k.to_s.to_f }
|
|
456
|
+
|
|
457
|
+
if strikes.empty?
|
|
458
|
+
puts "❌ No strikes found in chain data"
|
|
459
|
+
puts " Chain keys: #{chain.keys.first(5).inspect}" if chain.is_a?(Hash)
|
|
460
|
+
else
|
|
461
|
+
# Find ATM strike (closest to underlying price)
|
|
462
|
+
atm_strike = strikes.min_by { |s| (s - underlying_price).abs }
|
|
463
|
+
atm_index = strikes.index(atm_strike)
|
|
464
|
+
atm_plus_one = strikes[atm_index + 1] if atm_index && (atm_index + 1) < strikes.length
|
|
465
|
+
|
|
466
|
+
puts "ATM Strike: ₹#{atm_strike}"
|
|
467
|
+
puts "ATM+1 Strike: ₹#{atm_plus_one || 'N/A'}\n"
|
|
468
|
+
|
|
469
|
+
# Extract data for ATM and ATM+1 strikes
|
|
470
|
+
# Match strike values to actual keys in chain hash
|
|
471
|
+
[atm_strike, atm_plus_one].compact.each do |strike_value|
|
|
472
|
+
print_strike_summary(strike_value, strike_keys, chain)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
puts "=" * 60
|
|
476
|
+
puts "✅ Successfully extracted ATM and ATM+1 strikes for CALL and PUT"
|
|
477
|
+
end
|
|
478
|
+
else
|
|
479
|
+
puts "❌ Unexpected response format"
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
rescue StandardError => e
|
|
483
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
484
|
+
puts e.backtrace.first(5).join("\n")
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
puts "\n" + ("=" * 60)
|
|
488
|
+
puts "--- Test 5: Direct chat_raw() Test ---"
|
|
489
|
+
puts "Testing chat_raw() to access tool_calls directly\n"
|
|
490
|
+
|
|
491
|
+
begin
|
|
492
|
+
response = client.chat_raw(
|
|
493
|
+
model: ENV.fetch("OLLAMA_MODEL", "llama3.1:8b"),
|
|
494
|
+
messages: [Ollama::Agent::Messages.user("Get the option chain for SENSEX index")],
|
|
495
|
+
tools: option_chain_tool,
|
|
496
|
+
allow_chat: true
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
puts "✅ Response received"
|
|
500
|
+
puts "Response class: #{response.class.name}"
|
|
501
|
+
|
|
502
|
+
# Method access (like ollama-ruby)
|
|
503
|
+
tool_calls = response.message&.tool_calls
|
|
504
|
+
if tool_calls && !tool_calls.empty?
|
|
505
|
+
puts "\n✅ Tool calls detected (via method access):"
|
|
506
|
+
tool_calls.each do |call|
|
|
507
|
+
puts " Tool: #{call.name}"
|
|
508
|
+
puts " Arguments: #{call.arguments.inspect}"
|
|
509
|
+
puts " ID: #{call.id || 'N/A'}"
|
|
510
|
+
end
|
|
511
|
+
else
|
|
512
|
+
puts "\n⚠️ No tool calls detected"
|
|
513
|
+
puts "Content: #{response.message&.content}"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Hash access (backward compatible)
|
|
517
|
+
tool_calls_hash = response.to_h.dig("message", "tool_calls")
|
|
518
|
+
if tool_calls_hash && !tool_calls_hash.empty?
|
|
519
|
+
puts "\n✅ Tool calls also accessible via hash:"
|
|
520
|
+
puts " Count: #{tool_calls_hash.length}"
|
|
521
|
+
end
|
|
522
|
+
rescue Ollama::Error => e
|
|
523
|
+
puts "\n❌ Error: #{e.class.name}"
|
|
524
|
+
puts " Message: #{e.message}"
|
|
525
|
+
rescue StandardError => e
|
|
526
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
puts "\n" + ("=" * 60)
|
|
530
|
+
puts "--- Summary ---"
|
|
531
|
+
puts "✅ Tool calling with Executor: Working"
|
|
532
|
+
puts "✅ Structured Tool classes: Working"
|
|
533
|
+
puts "✅ chat_raw() method access: Working"
|
|
534
|
+
puts "✅ Hash access (backward compatible): Working"
|
|
535
|
+
puts "✅ Option chain expiry list: Working"
|
|
536
|
+
puts "✅ Option chain full data with strikes: Working"
|
|
537
|
+
puts "✅ ATM/ATM+1 strike extraction: Working"
|
|
538
|
+
puts "\n=== DONE ===\n"
|