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
|
@@ -59,6 +59,139 @@ module DhanHQ
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
def price_range_stats(price_ranges)
|
|
63
|
+
return nil unless price_ranges.is_a?(Array) && price_ranges.any?
|
|
64
|
+
|
|
65
|
+
{
|
|
66
|
+
min: price_ranges.min.round(2),
|
|
67
|
+
max: price_ranges.max.round(2),
|
|
68
|
+
avg: (price_ranges.sum / price_ranges.length).round(2),
|
|
69
|
+
count: price_ranges.length
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_expired_options_summary(stats)
|
|
74
|
+
{
|
|
75
|
+
data_points: stats[:data_points] || 0,
|
|
76
|
+
avg_volume: stats[:avg_volume]&.round(2),
|
|
77
|
+
avg_open_interest: stats[:avg_open_interest]&.round(2),
|
|
78
|
+
avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
|
|
79
|
+
price_range_stats: price_range_stats(stats[:price_ranges]),
|
|
80
|
+
has_ohlc: stats[:has_ohlc],
|
|
81
|
+
has_volume: stats[:has_volume],
|
|
82
|
+
has_open_interest: stats[:has_open_interest],
|
|
83
|
+
has_implied_volatility: stats[:has_implied_volatility]
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_option_chain_summary(chain_result)
|
|
88
|
+
chain = chain_result[:result][:chain]
|
|
89
|
+
underlying_price = chain_result[:result][:underlying_last_price]
|
|
90
|
+
|
|
91
|
+
unless chain.is_a?(Hash)
|
|
92
|
+
return [{ expiry: chain_result[:result][:expiry], chain_type: chain.class },
|
|
93
|
+
underlying_price]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
strike_prices = chain.keys.sort_by(&:to_f)
|
|
97
|
+
first_strike_data = strike_prices.any? ? chain[strike_prices.first] : nil
|
|
98
|
+
atm_strike = select_atm_strike(strike_prices, underlying_price)
|
|
99
|
+
atm_data = atm_strike ? chain[atm_strike] : nil
|
|
100
|
+
sample_greeks = build_sample_greeks(atm_data, atm_strike)
|
|
101
|
+
|
|
102
|
+
summary = {
|
|
103
|
+
expiry: chain_result[:result][:expiry],
|
|
104
|
+
underlying_last_price: underlying_price,
|
|
105
|
+
strikes_count: strike_prices.length,
|
|
106
|
+
has_call_options: option_type_present?(first_strike_data, "ce"),
|
|
107
|
+
has_put_options: option_type_present?(first_strike_data, "pe"),
|
|
108
|
+
has_greeks: sample_greeks.any?,
|
|
109
|
+
strike_range: strike_range_summary(strike_prices),
|
|
110
|
+
sample_greeks: sample_greeks.any? ? sample_greeks : nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
[summary, underlying_price]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def select_atm_strike(strike_prices, underlying_price)
|
|
117
|
+
return strike_prices.first unless underlying_price && strike_prices.any?
|
|
118
|
+
|
|
119
|
+
strike_prices.min_by { |strike| (strike.to_f - underlying_price).abs }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def option_type_present?(strike_data, key)
|
|
123
|
+
strike_data.is_a?(Hash) && (strike_data.key?(key) || strike_data.key?(key.to_sym))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def strike_range_summary(strike_prices)
|
|
127
|
+
return nil if strike_prices.empty?
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
min: strike_prices.first,
|
|
131
|
+
max: strike_prices.last,
|
|
132
|
+
sample_strikes: strike_prices.first(5)
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_sample_greeks(atm_data, atm_strike)
|
|
137
|
+
return {} unless atm_data.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
sample = {}
|
|
140
|
+
call_data = atm_data["ce"] || atm_data[:ce]
|
|
141
|
+
put_data = atm_data["pe"] || atm_data[:pe]
|
|
142
|
+
|
|
143
|
+
call_greeks = extract_greeks(call_data)
|
|
144
|
+
sample[:call] = greeks_summary(call_greeks, call_data, atm_strike) if call_greeks
|
|
145
|
+
|
|
146
|
+
put_greeks = extract_greeks(put_data)
|
|
147
|
+
sample[:put] = greeks_summary(put_greeks, put_data, atm_strike) if put_greeks
|
|
148
|
+
|
|
149
|
+
sample
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def extract_greeks(option_data)
|
|
153
|
+
return nil unless option_data.is_a?(Hash)
|
|
154
|
+
return nil unless option_data.key?("greeks") || option_data.key?(:greeks)
|
|
155
|
+
|
|
156
|
+
option_data["greeks"] || option_data[:greeks]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def greeks_summary(greeks, option_data, atm_strike)
|
|
160
|
+
{
|
|
161
|
+
strike: atm_strike,
|
|
162
|
+
delta: greeks["delta"] || greeks[:delta],
|
|
163
|
+
theta: greeks["theta"] || greeks[:theta],
|
|
164
|
+
gamma: greeks["gamma"] || greeks[:gamma],
|
|
165
|
+
vega: greeks["vega"] || greeks[:vega],
|
|
166
|
+
iv: option_data["implied_volatility"] || option_data[:implied_volatility],
|
|
167
|
+
oi: option_data["oi"] || option_data[:oi],
|
|
168
|
+
last_price: option_data["last_price"] || option_data[:last_price]
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def format_score_breakdown(details)
|
|
173
|
+
"Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
|
|
174
|
+
"Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def format_option_setup_details(setup)
|
|
178
|
+
iv = setup[:iv]&.round(2) || "N/A"
|
|
179
|
+
oi = setup[:oi] || "N/A"
|
|
180
|
+
volume = setup[:volume] || "N/A"
|
|
181
|
+
"IV: #{iv}% | OI: #{oi} | Volume: #{volume}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def handle_option_chain_result(chain_result)
|
|
185
|
+
if chain_result[:result] && chain_result[:result][:chain]
|
|
186
|
+
chain_summary, underlying_price = build_option_chain_summary(chain_result)
|
|
187
|
+
puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
|
|
188
|
+
puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
|
|
189
|
+
puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
|
|
190
|
+
elsif chain_result[:error]
|
|
191
|
+
puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
62
195
|
# Main execution
|
|
63
196
|
if __FILE__ == $PROGRAM_NAME
|
|
64
197
|
# Configure DhanHQ
|
|
@@ -267,7 +400,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
267
400
|
puts " ✅ Historical data retrieved"
|
|
268
401
|
puts " 📊 Type: #{result[:type]}"
|
|
269
402
|
puts " 📊 Records: #{result[:result][:count]}"
|
|
270
|
-
if result[:result][:count]
|
|
403
|
+
if result[:result][:count].zero?
|
|
271
404
|
puts " ⚠️ No data found for date range #{from_date} to #{to_date}"
|
|
272
405
|
puts " (This may be normal if market was closed or data unavailable)"
|
|
273
406
|
end
|
|
@@ -300,26 +433,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
300
433
|
puts " 📊 Expiry: #{result[:result][:expiry_date]}"
|
|
301
434
|
if result[:result][:summary_stats]
|
|
302
435
|
stats = result[:result][:summary_stats]
|
|
303
|
-
concise_summary =
|
|
304
|
-
data_points: stats[:data_points] || 0,
|
|
305
|
-
avg_volume: stats[:avg_volume]&.round(2),
|
|
306
|
-
avg_open_interest: stats[:avg_open_interest]&.round(2),
|
|
307
|
-
avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
|
|
308
|
-
price_range_stats: if stats[:price_ranges]&.is_a?(Array) && !stats[:price_ranges].empty?
|
|
309
|
-
{
|
|
310
|
-
min: stats[:price_ranges].min.round(2),
|
|
311
|
-
max: stats[:price_ranges].max.round(2),
|
|
312
|
-
avg: (stats[:price_ranges].sum / stats[:price_ranges].length).round(2),
|
|
313
|
-
count: stats[:price_ranges].length
|
|
314
|
-
}
|
|
315
|
-
else
|
|
316
|
-
nil
|
|
317
|
-
end,
|
|
318
|
-
has_ohlc: stats[:has_ohlc],
|
|
319
|
-
has_volume: stats[:has_volume],
|
|
320
|
-
has_open_interest: stats[:has_open_interest],
|
|
321
|
-
has_implied_volatility: stats[:has_implied_volatility]
|
|
322
|
-
}
|
|
436
|
+
concise_summary = build_expired_options_summary(stats)
|
|
323
437
|
puts " 📊 Data summary: #{JSON.pretty_generate(concise_summary)}"
|
|
324
438
|
else
|
|
325
439
|
puts " 📊 Data available but summary stats not found"
|
|
@@ -355,82 +469,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
355
469
|
exchange_segment: "IDX_I",
|
|
356
470
|
expiry: next_expiry
|
|
357
471
|
)
|
|
358
|
-
|
|
359
|
-
chain = chain_result[:result][:chain]
|
|
360
|
-
underlying_price = chain_result[:result][:underlying_last_price]
|
|
361
|
-
|
|
362
|
-
chain_summary = if chain.is_a?(Hash)
|
|
363
|
-
strike_prices = chain.keys.sort_by { |k| k.to_f }
|
|
364
|
-
first_strike_data = chain[strike_prices.first] unless strike_prices.empty?
|
|
365
|
-
|
|
366
|
-
atm_strike = if underlying_price && !strike_prices.empty?
|
|
367
|
-
strike_prices.min_by { |s| (s.to_f - underlying_price).abs }
|
|
368
|
-
else
|
|
369
|
-
strike_prices.first
|
|
370
|
-
end
|
|
371
|
-
atm_data = chain[atm_strike] if atm_strike
|
|
372
|
-
|
|
373
|
-
sample_greeks = {}
|
|
374
|
-
if atm_data.is_a?(Hash)
|
|
375
|
-
ce_data = atm_data["ce"] || atm_data[:ce]
|
|
376
|
-
pe_data = atm_data["pe"] || atm_data[:pe]
|
|
377
|
-
|
|
378
|
-
if ce_data.is_a?(Hash) && (ce_data.key?("greeks") || ce_data.key?(:greeks))
|
|
379
|
-
ce_greeks = ce_data["greeks"] || ce_data[:greeks]
|
|
380
|
-
sample_greeks[:call] = {
|
|
381
|
-
strike: atm_strike,
|
|
382
|
-
delta: ce_greeks["delta"] || ce_greeks[:delta],
|
|
383
|
-
theta: ce_greeks["theta"] || ce_greeks[:theta],
|
|
384
|
-
gamma: ce_greeks["gamma"] || ce_greeks[:gamma],
|
|
385
|
-
vega: ce_greeks["vega"] || ce_greeks[:vega],
|
|
386
|
-
iv: ce_data["implied_volatility"] || ce_data[:implied_volatility],
|
|
387
|
-
oi: ce_data["oi"] || ce_data[:oi],
|
|
388
|
-
last_price: ce_data["last_price"] || ce_data[:last_price]
|
|
389
|
-
}
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
if pe_data.is_a?(Hash) && (pe_data.key?("greeks") || pe_data.key?(:greeks))
|
|
393
|
-
pe_greeks = pe_data["greeks"] || pe_data[:greeks]
|
|
394
|
-
sample_greeks[:put] = {
|
|
395
|
-
strike: atm_strike,
|
|
396
|
-
delta: pe_greeks["delta"] || pe_greeks[:delta],
|
|
397
|
-
theta: pe_greeks["theta"] || pe_greeks[:theta],
|
|
398
|
-
gamma: pe_greeks["gamma"] || pe_greeks[:gamma],
|
|
399
|
-
vega: pe_greeks["vega"] || pe_greeks[:vega],
|
|
400
|
-
iv: pe_data["implied_volatility"] || pe_data[:implied_volatility],
|
|
401
|
-
oi: pe_data["oi"] || pe_data[:oi],
|
|
402
|
-
last_price: pe_data["last_price"] || pe_data[:last_price]
|
|
403
|
-
}
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
{
|
|
408
|
-
expiry: chain_result[:result][:expiry],
|
|
409
|
-
underlying_last_price: underlying_price,
|
|
410
|
-
strikes_count: strike_prices.length,
|
|
411
|
-
has_call_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("ce") || first_strike_data.key?(:ce)),
|
|
412
|
-
has_put_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("pe") || first_strike_data.key?(:pe)),
|
|
413
|
-
has_greeks: !sample_greeks.empty?,
|
|
414
|
-
strike_range: if strike_prices.empty?
|
|
415
|
-
nil
|
|
416
|
-
else
|
|
417
|
-
{
|
|
418
|
-
min: strike_prices.first,
|
|
419
|
-
max: strike_prices.last,
|
|
420
|
-
sample_strikes: strike_prices.first(5)
|
|
421
|
-
}
|
|
422
|
-
end,
|
|
423
|
-
sample_greeks: sample_greeks.empty? ? nil : sample_greeks
|
|
424
|
-
}
|
|
425
|
-
else
|
|
426
|
-
{ expiry: chain_result[:result][:expiry], chain_type: chain.class }
|
|
427
|
-
end
|
|
428
|
-
puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
|
|
429
|
-
puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
|
|
430
|
-
puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
|
|
431
|
-
elsif chain_result[:error]
|
|
432
|
-
puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
|
|
433
|
-
end
|
|
472
|
+
handle_option_chain_result(chain_result)
|
|
434
473
|
end
|
|
435
474
|
elsif expiry_list_result[:error]
|
|
436
475
|
puts " ⚠️ #{expiry_list_result[:error]}"
|
|
@@ -573,7 +612,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
573
612
|
puts " 📈 #{candidate[:symbol]}: Score #{candidate[:score]}/100"
|
|
574
613
|
if candidate[:score_details]
|
|
575
614
|
details = candidate[:score_details]
|
|
576
|
-
puts " Breakdown:
|
|
615
|
+
puts " Breakdown: #{format_score_breakdown(details)}"
|
|
577
616
|
end
|
|
578
617
|
trend = candidate[:analysis][:trend]
|
|
579
618
|
puts " Trend: #{trend[:trend]} (#{trend[:strength]}% strength)"
|
|
@@ -605,7 +644,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
605
644
|
puts " ✅ Found #{options_setups[:setups].length} options setups:"
|
|
606
645
|
options_setups[:setups].each do |setup|
|
|
607
646
|
puts " 📊 #{setup[:type].to_s.upcase} @ #{setup[:strike]}"
|
|
608
|
-
puts "
|
|
647
|
+
puts " #{format_option_setup_details(setup)}"
|
|
609
648
|
puts " Score: #{setup[:score]}/100 | Recommendation: #{setup[:recommendation]}"
|
|
610
649
|
end
|
|
611
650
|
else
|
|
@@ -617,6 +656,163 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
617
656
|
puts " #{e.backtrace.first(3).join("\n ")}" if e.backtrace
|
|
618
657
|
end
|
|
619
658
|
|
|
659
|
+
puts
|
|
660
|
+
puts "=" * 60
|
|
661
|
+
puts "TOOL CALLING EXAMPLE (Using Executor + Structured Tool Classes)"
|
|
662
|
+
puts "=" * 60
|
|
663
|
+
puts
|
|
664
|
+
|
|
665
|
+
# ============================================================
|
|
666
|
+
# TOOL CALLING WITH EXECUTOR
|
|
667
|
+
# ============================================================
|
|
668
|
+
puts "Example: Agentic Tool Calling with DhanHQ Tools"
|
|
669
|
+
puts "─" * 60
|
|
670
|
+
puts "This demonstrates the new tool calling pattern using:"
|
|
671
|
+
puts " - Structured Tool classes (type-safe schemas)"
|
|
672
|
+
puts " - Executor (automatic tool execution loop)"
|
|
673
|
+
puts " - chat_raw() internally (via Executor)"
|
|
674
|
+
puts
|
|
675
|
+
|
|
676
|
+
# Define DhanHQ tools using structured Tool classes
|
|
677
|
+
market_quote_tool = Ollama::Tool.new(
|
|
678
|
+
type: "function",
|
|
679
|
+
function: Ollama::Tool::Function.new(
|
|
680
|
+
name: "get_market_quote",
|
|
681
|
+
description: "Get market quote for a symbol. Returns OHLC, depth, volume, and other market data. " \
|
|
682
|
+
"Finds instrument automatically using exchange_segment and symbol.",
|
|
683
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
684
|
+
type: "object",
|
|
685
|
+
properties: {
|
|
686
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
687
|
+
type: "string",
|
|
688
|
+
description: "Stock or index symbol (e.g., RELIANCE, NIFTY)"
|
|
689
|
+
),
|
|
690
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
691
|
+
type: "string",
|
|
692
|
+
description: "Exchange segment",
|
|
693
|
+
enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
|
|
694
|
+
)
|
|
695
|
+
},
|
|
696
|
+
required: %w[symbol exchange_segment]
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
live_ltp_tool = Ollama::Tool.new(
|
|
702
|
+
type: "function",
|
|
703
|
+
function: Ollama::Tool::Function.new(
|
|
704
|
+
name: "get_live_ltp",
|
|
705
|
+
description: "Get live last traded price (LTP) for a symbol. Fast API for current price. " \
|
|
706
|
+
"Finds instrument automatically using exchange_segment and symbol.",
|
|
707
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
711
|
+
type: "string",
|
|
712
|
+
description: "Stock or index symbol"
|
|
713
|
+
),
|
|
714
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
715
|
+
type: "string",
|
|
716
|
+
description: "Exchange segment",
|
|
717
|
+
enum: %w[NSE_EQ NSE_FNO BSE_EQ BSE_FNO IDX_I]
|
|
718
|
+
)
|
|
719
|
+
},
|
|
720
|
+
required: %w[symbol exchange_segment]
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# Define tools with structured Tool classes and callables
|
|
726
|
+
tools = {
|
|
727
|
+
"get_market_quote" => {
|
|
728
|
+
tool: market_quote_tool,
|
|
729
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
730
|
+
result = DhanHQDataTools.get_market_quote(
|
|
731
|
+
symbol: symbol.to_s,
|
|
732
|
+
exchange_segment: exchange_segment.to_s
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if result[:error]
|
|
736
|
+
{ error: result[:error] }
|
|
737
|
+
else
|
|
738
|
+
quote = result[:result][:quote]
|
|
739
|
+
{
|
|
740
|
+
symbol: symbol,
|
|
741
|
+
exchange_segment: exchange_segment,
|
|
742
|
+
last_price: quote[:last_price],
|
|
743
|
+
volume: quote[:volume],
|
|
744
|
+
ohlc: quote[:ohlc],
|
|
745
|
+
change_percent: quote[:net_change]
|
|
746
|
+
}
|
|
747
|
+
end
|
|
748
|
+
rescue StandardError => e
|
|
749
|
+
{ error: e.message }
|
|
750
|
+
end
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
"get_live_ltp" => {
|
|
754
|
+
tool: live_ltp_tool,
|
|
755
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
756
|
+
result = DhanHQDataTools.get_live_ltp(
|
|
757
|
+
symbol: symbol.to_s,
|
|
758
|
+
exchange_segment: exchange_segment.to_s
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if result[:error]
|
|
762
|
+
{ error: result[:error] }
|
|
763
|
+
else
|
|
764
|
+
{
|
|
765
|
+
symbol: symbol,
|
|
766
|
+
exchange_segment: exchange_segment,
|
|
767
|
+
ltp: result[:result][:ltp],
|
|
768
|
+
timestamp: result[:result][:timestamp]
|
|
769
|
+
}
|
|
770
|
+
end
|
|
771
|
+
rescue StandardError => e
|
|
772
|
+
{ error: e.message }
|
|
773
|
+
end
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
# Create executor with tools
|
|
778
|
+
# Create client with same configuration as other examples
|
|
779
|
+
executor_config = Ollama::Config.new
|
|
780
|
+
executor_config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
|
|
781
|
+
executor_config.temperature = 0.2
|
|
782
|
+
executor_config.timeout = 60
|
|
783
|
+
executor_client = Ollama::Client.new(config: executor_config)
|
|
784
|
+
|
|
785
|
+
executor = Ollama::Agent::Executor.new(
|
|
786
|
+
executor_client,
|
|
787
|
+
tools: tools,
|
|
788
|
+
max_steps: 10
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
begin
|
|
792
|
+
puts "🔄 Starting agentic tool-calling loop..."
|
|
793
|
+
puts "User query: Get market quote for RELIANCE and also check NIFTY's current price"
|
|
794
|
+
puts
|
|
795
|
+
|
|
796
|
+
result = executor.run(
|
|
797
|
+
system: "You are a market data assistant. Use the available tools to get market data. " \
|
|
798
|
+
"You can call multiple tools in sequence. When you have the data, summarize it for the user.",
|
|
799
|
+
user: "Get market quote for RELIANCE stock and also check NIFTY's current price"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
puts
|
|
803
|
+
puts "=" * 60
|
|
804
|
+
puts "Tool Calling Result:"
|
|
805
|
+
puts "=" * 60
|
|
806
|
+
puts result
|
|
807
|
+
puts
|
|
808
|
+
rescue Ollama::Error => e
|
|
809
|
+
puts "❌ Error: #{e.message}"
|
|
810
|
+
puts e.backtrace.first(5).join("\n") if e.backtrace
|
|
811
|
+
rescue StandardError => e
|
|
812
|
+
puts "❌ Unexpected error: #{e.message}"
|
|
813
|
+
puts e.backtrace.first(3).join("\n") if e.backtrace
|
|
814
|
+
end
|
|
815
|
+
|
|
620
816
|
puts
|
|
621
817
|
puts "=" * 60
|
|
622
818
|
puts "DhanHQ Agent Summary:"
|
|
@@ -628,5 +824,6 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
628
824
|
puts " ✅ Technical Analysis: Trend analysis, SMC concepts, Pattern recognition, Indicators (RSI, MACD, MA, etc.)"
|
|
629
825
|
puts " ✅ Scanners: Swing trading scanner, Intraday options scanner"
|
|
630
826
|
puts " ✅ Analysis Agents: Technical analysis agent with LLM interpretation"
|
|
827
|
+
puts " ✅ Tool Calling: Executor with structured Tool classes (NEW!)"
|
|
631
828
|
puts "=" * 60
|
|
632
829
|
end
|
|
@@ -35,8 +35,8 @@ module DhanHQ
|
|
|
35
35
|
|
|
36
36
|
prices.each_cons(2) do |prev, curr|
|
|
37
37
|
change = curr - prev
|
|
38
|
-
gains <<
|
|
39
|
-
losses << (change
|
|
38
|
+
gains << [change, 0].max
|
|
39
|
+
losses << (change.negative? ? -change : 0)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
rsi_values = []
|
|
@@ -81,7 +81,7 @@ module DhanHQ
|
|
|
81
81
|
sma_values = sma(prices, period)
|
|
82
82
|
bands = { upper: [], middle: sma_values, lower: [] }
|
|
83
83
|
|
|
84
|
-
prices.each_cons(period).
|
|
84
|
+
prices.each_cons(period).with_index do |window, idx|
|
|
85
85
|
mean = sma_values[idx]
|
|
86
86
|
variance = window.map { |p| (p - mean)**2 }.sum / period
|
|
87
87
|
std = Math.sqrt(variance)
|
|
@@ -147,8 +147,6 @@ module DhanHQ
|
|
|
147
147
|
{ support: support_levels.uniq { |s| s[:price] }, resistance: resistance_levels.uniq { |r| r[:price] } }
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
private
|
|
151
|
-
|
|
152
150
|
def self.calculate_rsi_value(avg_gain, avg_loss)
|
|
153
151
|
return 100 if avg_loss.zero?
|
|
154
152
|
|