ollama-client 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +336 -91
  4. data/RELEASE_NOTES_v0.2.6.md +41 -0
  5. data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
  6. data/docs/EXAMPLE_REORGANIZATION.md +412 -0
  7. data/docs/FEATURES_ADDED.md +12 -1
  8. data/docs/GETTING_STARTED.md +361 -0
  9. data/docs/INTEGRATION_TESTING.md +170 -0
  10. data/docs/NEXT_STEPS_SUMMARY.md +114 -0
  11. data/docs/PERSONAS.md +383 -0
  12. data/docs/QUICK_START.md +195 -0
  13. data/docs/TESTING.md +392 -170
  14. data/docs/TEST_CHECKLIST.md +450 -0
  15. data/examples/README.md +62 -63
  16. data/examples/basic_chat.rb +33 -0
  17. data/examples/basic_generate.rb +29 -0
  18. data/examples/mcp_executor.rb +39 -0
  19. data/examples/mcp_http_executor.rb +45 -0
  20. data/examples/tool_calling_parsing.rb +59 -0
  21. data/examples/tool_dto_example.rb +0 -0
  22. data/exe/ollama-client +128 -1
  23. data/lib/ollama/agent/planner.rb +7 -2
  24. data/lib/ollama/chat_session.rb +101 -0
  25. data/lib/ollama/client.rb +41 -35
  26. data/lib/ollama/config.rb +9 -4
  27. data/lib/ollama/document_loader.rb +1 -1
  28. data/lib/ollama/embeddings.rb +61 -28
  29. data/lib/ollama/errors.rb +1 -0
  30. data/lib/ollama/mcp/http_client.rb +149 -0
  31. data/lib/ollama/mcp/stdio_client.rb +146 -0
  32. data/lib/ollama/mcp/tools_bridge.rb +72 -0
  33. data/lib/ollama/mcp.rb +31 -0
  34. data/lib/ollama/options.rb +3 -1
  35. data/lib/ollama/personas.rb +287 -0
  36. data/lib/ollama/version.rb +1 -1
  37. data/lib/ollama_client.rb +17 -5
  38. metadata +22 -48
  39. data/examples/advanced_complex_schemas.rb +0 -366
  40. data/examples/advanced_edge_cases.rb +0 -241
  41. data/examples/advanced_error_handling.rb +0 -200
  42. data/examples/advanced_multi_step_agent.rb +0 -341
  43. data/examples/advanced_performance_testing.rb +0 -186
  44. data/examples/chat_console.rb +0 -143
  45. data/examples/complete_workflow.rb +0 -245
  46. data/examples/dhan_console.rb +0 -843
  47. data/examples/dhanhq/README.md +0 -236
  48. data/examples/dhanhq/agents/base_agent.rb +0 -74
  49. data/examples/dhanhq/agents/data_agent.rb +0 -66
  50. data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
  51. data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
  52. data/examples/dhanhq/agents/trading_agent.rb +0 -81
  53. data/examples/dhanhq/analysis/market_structure.rb +0 -138
  54. data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
  55. data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
  56. data/examples/dhanhq/builders/market_context_builder.rb +0 -67
  57. data/examples/dhanhq/dhanhq_agent.rb +0 -829
  58. data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
  59. data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
  60. data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
  61. data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
  62. data/examples/dhanhq/services/base_service.rb +0 -46
  63. data/examples/dhanhq/services/data_service.rb +0 -118
  64. data/examples/dhanhq/services/trading_service.rb +0 -59
  65. data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
  66. data/examples/dhanhq/technical_analysis_runner.rb +0 -420
  67. data/examples/dhanhq/test_tool_calling.rb +0 -538
  68. data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
  69. data/examples/dhanhq/utils/instrument_helper.rb +0 -32
  70. data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
  71. data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
  72. data/examples/dhanhq/utils/rate_limiter.rb +0 -23
  73. data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
  74. data/examples/dhanhq_agent.rb +0 -964
  75. data/examples/dhanhq_tools.rb +0 -1663
  76. data/examples/multi_step_agent_with_external_data.rb +0 -368
  77. data/examples/structured_outputs_chat.rb +0 -72
  78. data/examples/structured_tools.rb +0 -89
  79. data/examples/test_dhanhq_tool_calling.rb +0 -375
  80. data/examples/test_tool_calling.rb +0 -160
  81. data/examples/tool_calling_direct.rb +0 -124
  82. data/examples/tool_calling_pattern.rb +0 -269
  83. data/exe/dhan_console +0 -4
@@ -1,964 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # DhanHQ Agent - Complete trading agent with data retrieval and trading operations
5
- # This agent can:
6
- # - Fetch and analyze market data (6 Data APIs)
7
- # - Build order parameters for trading (does not place orders)
8
-
9
- require "json"
10
- require "date"
11
- require "dhan_hq"
12
- require_relative "../lib/ollama_client"
13
- require_relative "dhanhq_tools"
14
-
15
- # Helper to build market context from data
16
- # rubocop:disable Metrics/PerceivedComplexity
17
- def build_market_context_from_data(market_data)
18
- context_parts = []
19
-
20
- if market_data[:nifty]
21
- ltp = market_data[:nifty][:ltp]
22
- change = market_data[:nifty][:change_percent]
23
- context_parts << if ltp && ltp != 0
24
- "NIFTY is trading at #{ltp} (#{change || 'unknown'}% change)"
25
- else
26
- "NIFTY data retrieved but LTP is not available (may be outside market hours)"
27
- end
28
- else
29
- context_parts << "NIFTY data not available"
30
- end
31
-
32
- if market_data[:reliance]
33
- ltp = market_data[:reliance][:ltp]
34
- change = market_data[:reliance][:change_percent]
35
- volume = market_data[:reliance][:volume]
36
- context_parts << if ltp && ltp != 0
37
- "RELIANCE is at #{ltp} (#{change || 'unknown'}% change, Volume: #{volume || 'N/A'})"
38
- else
39
- "RELIANCE data retrieved but LTP is not available (may be outside market hours)"
40
- end
41
- else
42
- context_parts << "RELIANCE data not available"
43
- end
44
-
45
- if market_data[:positions] && !market_data[:positions].empty?
46
- context_parts << "Current positions: #{market_data[:positions].length} active"
47
- market_data[:positions].each do |pos|
48
- context_parts << " - #{pos[:trading_symbol]}: #{pos[:quantity]} @ #{pos[:average_price]}"
49
- end
50
- else
51
- context_parts << "Current positions: None"
52
- end
53
-
54
- context_parts.join("\n")
55
- end
56
- # rubocop:enable Metrics/PerceivedComplexity
57
-
58
- # Data-focused Agent using Ollama for reasoning
59
- class DataAgent
60
- def initialize(ollama_client:)
61
- @ollama_client = ollama_client
62
- @decision_schema = {
63
- "type" => "object",
64
- "required" => ["action", "reasoning", "confidence"],
65
- "properties" => {
66
- "action" => {
67
- "type" => "string",
68
- "enum" => ["get_market_quote", "get_live_ltp", "get_market_depth", "get_historical_data",
69
- "get_expired_options_data", "get_option_chain", "no_action"]
70
- },
71
- "reasoning" => {
72
- "type" => "string",
73
- "description" => "Why this action was chosen"
74
- },
75
- "confidence" => {
76
- "type" => "number",
77
- "minimum" => 0,
78
- "maximum" => 1,
79
- "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
80
- },
81
- "parameters" => {
82
- "type" => "object",
83
- "additionalProperties" => true,
84
- "description" => "Parameters for the action (symbol, exchange_segment, etc.)"
85
- }
86
- }
87
- }
88
- end
89
-
90
- def analyze_and_decide(market_context:)
91
- prompt = build_analysis_prompt(market_context: market_context)
92
-
93
- begin
94
- decision = @ollama_client.generate(
95
- prompt: prompt,
96
- schema: @decision_schema
97
- )
98
-
99
- # Validate confidence threshold
100
- return { action: "no_action", reason: "invalid_decision" } unless decision.is_a?(Hash) && decision["confidence"]
101
-
102
- if decision["confidence"] < 0.6
103
- puts "⚠️ Low confidence (#{(decision["confidence"] * 100).round}%) - skipping action"
104
- return { action: "no_action", reason: "low_confidence" }
105
- end
106
-
107
- decision
108
- rescue Ollama::Error => e
109
- puts "❌ Ollama error: #{e.message}"
110
- { action: "no_action", reason: "error", error: e.message }
111
- rescue StandardError => e
112
- puts "❌ Unexpected error: #{e.message}"
113
- { action: "no_action", reason: "error", error: e.message }
114
- end
115
- end
116
-
117
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
118
- def execute_decision(decision)
119
- action = decision["action"]
120
- params = normalize_parameters(decision["parameters"] || {})
121
-
122
- case action
123
- when "get_market_quote"
124
- if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
125
- { action: "get_market_quote", error: "Either symbol or security_id is required", params: params }
126
- else
127
- DhanHQDataTools.get_market_quote(
128
- symbol: params["symbol"],
129
- security_id: params["security_id"],
130
- exchange_segment: params["exchange_segment"] || "NSE_EQ"
131
- )
132
- end
133
-
134
- when "get_live_ltp"
135
- if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
136
- { action: "get_live_ltp", error: "Either symbol or security_id is required", params: params }
137
- else
138
- DhanHQDataTools.get_live_ltp(
139
- symbol: params["symbol"],
140
- security_id: params["security_id"],
141
- exchange_segment: params["exchange_segment"] || "NSE_EQ"
142
- )
143
- end
144
-
145
- when "get_market_depth"
146
- if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
147
- { action: "get_market_depth", error: "Either symbol or security_id is required", params: params }
148
- else
149
- DhanHQDataTools.get_market_depth(
150
- symbol: params["symbol"],
151
- security_id: params["security_id"],
152
- exchange_segment: params["exchange_segment"] || "NSE_EQ"
153
- )
154
- end
155
-
156
- when "get_historical_data"
157
- if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
158
- { action: "get_historical_data", error: "Either symbol or security_id is required", params: params }
159
- else
160
- DhanHQDataTools.get_historical_data(
161
- symbol: params["symbol"],
162
- security_id: params["security_id"],
163
- exchange_segment: params["exchange_segment"] || "NSE_EQ",
164
- from_date: params["from_date"],
165
- to_date: params["to_date"],
166
- interval: params["interval"],
167
- expiry_code: params["expiry_code"]
168
- )
169
- end
170
-
171
- when "get_option_chain"
172
- if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
173
- { action: "get_option_chain", error: "Either symbol or security_id is required", params: params }
174
- else
175
- DhanHQDataTools.get_option_chain(
176
- symbol: params["symbol"],
177
- security_id: params["security_id"],
178
- exchange_segment: params["exchange_segment"] || "NSE_EQ",
179
- expiry: params["expiry"]
180
- )
181
- end
182
-
183
- when "get_expired_options_data"
184
- symbol_or_id_missing = params["symbol"].nil? &&
185
- (params["security_id"].nil? || params["security_id"].to_s.empty?)
186
- if symbol_or_id_missing || params["expiry_date"].nil?
187
- {
188
- action: "get_expired_options_data",
189
- error: "Either symbol or security_id, and expiry_date are required",
190
- params: params
191
- }
192
- else
193
- DhanHQDataTools.get_expired_options_data(
194
- symbol: params["symbol"],
195
- security_id: params["security_id"],
196
- exchange_segment: params["exchange_segment"] || "NSE_FNO",
197
- expiry_date: params["expiry_date"],
198
- expiry_code: params["expiry_code"],
199
- interval: params["interval"] || "1",
200
- instrument: params["instrument"],
201
- expiry_flag: params["expiry_flag"] || "MONTH",
202
- strike: params["strike"] || "ATM",
203
- drv_option_type: params["drv_option_type"] || "CALL",
204
- required_data: params["required_data"]
205
- )
206
- end
207
-
208
- when "no_action"
209
- { action: "no_action", message: "No action taken" }
210
-
211
- else
212
- { action: "unknown", error: "Unknown action: #{action}" }
213
- end
214
- end
215
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
216
-
217
- private
218
-
219
- def normalize_parameters(params)
220
- normalized = {}
221
- params.each do |key, value|
222
- # For symbol and exchange_segment, extract first element if array
223
- # For other fields, preserve original type
224
- if %w[symbol exchange_segment].include?(key.to_s)
225
- normalized[key] = if value.is_a?(Array) && !value.empty?
226
- value.first.to_s
227
- elsif value.is_a?(String) && value.strip.start_with?("[") && value.strip.end_with?("]")
228
- # Handle stringified arrays
229
- begin
230
- parsed = JSON.parse(value)
231
- parsed.is_a?(Array) && !parsed.empty? ? parsed.first.to_s : value.to_s
232
- rescue JSON::ParserError
233
- value.to_s
234
- end
235
- else
236
- value.to_s
237
- end
238
- else
239
- # Preserve original type for other parameters
240
- normalized[key] = value
241
- end
242
- end
243
- normalized
244
- end
245
-
246
- def build_analysis_prompt(market_context:)
247
- <<~PROMPT
248
- Analyze the following market situation and decide the best data retrieval action:
249
-
250
- Market Context:
251
- #{market_context}
252
-
253
- Available Actions (DATA ONLY - NO TRADING):
254
- - get_market_quote: Get market quote using Instrument.quote convenience method (requires: symbol OR security_id as STRING, exchange_segment as STRING)
255
- - get_live_ltp: Get live last traded price using Instrument.ltp convenience method (requires: symbol OR security_id as STRING, exchange_segment as STRING)
256
- - get_market_depth: Get full market depth (bid/ask levels) using Instrument.quote convenience method (requires: symbol OR security_id as STRING, exchange_segment as STRING)
257
- - get_historical_data: Get historical data using Instrument.daily/intraday convenience methods (requires: symbol OR security_id as STRING, exchange_segment as STRING, from_date, to_date, optional: interval, expiry_code)
258
- - get_expired_options_data: Get expired options historical data (requires: symbol OR security_id as STRING, exchange_segment as STRING, expiry_date; optional: expiry_code, interval, instrument, expiry_flag, strike, drv_option_type, required_data)
259
- - get_option_chain: Get option chain using Instrument.expiry_list/option_chain convenience methods (requires: symbol OR security_id as STRING, exchange_segment as STRING, optional: expiry)
260
- - no_action: Take no action if unclear what data is needed
261
-
262
- CRITICAL: Each API call handles ONLY ONE symbol at a time. If you need data for multiple symbols, choose ONE symbol for this decision.
263
- - symbol must be a SINGLE STRING value (e.g., "NIFTY" or "RELIANCE"), NOT an array
264
- - exchange_segment must be a SINGLE STRING value (e.g., "NSE_EQ" or "IDX_I"), NOT an array
265
- - All APIs use Instrument.find() which expects SYMBOL (e.g., "NIFTY", "RELIANCE"), not security_id
266
- - Instrument convenience methods automatically use the instrument's security_id, exchange_segment, and instrument attributes
267
- - Use symbol when possible for better compatibility
268
- Examples:
269
- - For NIFTY: symbol="NIFTY", exchange_segment="IDX_I"
270
- - For RELIANCE: symbol="RELIANCE", exchange_segment="NSE_EQ"
271
- Valid exchange_segments: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
272
-
273
- Decision Criteria:
274
- - Only take actions with confidence > 0.6
275
- - Focus on data retrieval, not trading decisions
276
- - Provide all required parameters for the chosen action
277
-
278
- Respond with a JSON object containing:
279
- - action: one of the available actions
280
- - reasoning: why this action was chosen
281
- - confidence: your confidence level (0-1)
282
- - parameters: object with required parameters for the action
283
- PROMPT
284
- end
285
- end
286
-
287
- # Trading-focused Agent using Ollama for reasoning
288
- class TradingAgent
289
- def initialize(ollama_client:)
290
- @ollama_client = ollama_client
291
- @decision_schema = {
292
- "type" => "object",
293
- "required" => ["action", "reasoning", "confidence"],
294
- "properties" => {
295
- "action" => {
296
- "type" => "string",
297
- "enum" => ["place_order", "place_super_order", "cancel_order", "no_action"]
298
- },
299
- "reasoning" => {
300
- "type" => "string",
301
- "description" => "Why this action was chosen"
302
- },
303
- "confidence" => {
304
- "type" => "number",
305
- "minimum" => 0,
306
- "maximum" => 1,
307
- "description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
308
- },
309
- "parameters" => {
310
- "type" => "object",
311
- "additionalProperties" => true,
312
- "description" => "Parameters for the action (security_id, quantity, price, etc.)"
313
- }
314
- }
315
- }
316
- end
317
-
318
- def analyze_and_decide(market_context:)
319
- prompt = build_analysis_prompt(market_context: market_context)
320
-
321
- begin
322
- decision = @ollama_client.generate(
323
- prompt: prompt,
324
- schema: @decision_schema
325
- )
326
-
327
- # Clean up parameters - remove any keys that look like comments or instructions
328
- if decision.is_a?(Hash) && decision["parameters"].is_a?(Hash)
329
- decision["parameters"] = decision["parameters"].reject do |key, _value|
330
- key_str = key.to_s
331
- key_str.start_with?(">") || key_str.start_with?("//") || key_str.include?("adjust") || key_str.length > 50
332
- end
333
- end
334
-
335
- # Validate confidence threshold
336
- return { action: "no_action", reason: "invalid_decision" } unless decision.is_a?(Hash) && decision["confidence"]
337
-
338
- if decision["confidence"] < 0.6
339
- puts "⚠️ Low confidence (#{(decision["confidence"] * 100).round}%) - skipping action"
340
- return { action: "no_action", reason: "low_confidence" }
341
- end
342
-
343
- decision
344
- rescue Ollama::Error => e
345
- puts "❌ Ollama error: #{e.message}"
346
- { action: "no_action", reason: "error", error: e.message }
347
- rescue StandardError => e
348
- puts "❌ Unexpected error: #{e.message}"
349
- { action: "no_action", reason: "error", error: e.message }
350
- end
351
- end
352
-
353
- def execute_decision(decision)
354
- action = decision["action"]
355
- params = decision["parameters"] || {}
356
-
357
- case action
358
- when "place_order"
359
- handle_place_order(params)
360
- when "place_super_order"
361
- handle_place_super_order(params)
362
- when "cancel_order"
363
- handle_cancel_order(params)
364
- when "no_action"
365
- handle_no_action
366
- else
367
- handle_unknown_action(action)
368
- end
369
- end
370
-
371
- private
372
-
373
- def handle_place_order(params)
374
- DhanHQTradingTools.build_order_params(
375
- transaction_type: params["transaction_type"] || "BUY",
376
- exchange_segment: params["exchange_segment"] || "NSE_EQ",
377
- product_type: params["product_type"] || "MARGIN",
378
- order_type: params["order_type"] || "LIMIT",
379
- security_id: params["security_id"],
380
- quantity: params["quantity"] || 1,
381
- price: params["price"]
382
- )
383
- end
384
-
385
- def handle_place_super_order(params)
386
- DhanHQTradingTools.build_super_order_params(
387
- transaction_type: params["transaction_type"] || "BUY",
388
- exchange_segment: params["exchange_segment"] || "NSE_EQ",
389
- product_type: params["product_type"] || "MARGIN",
390
- order_type: params["order_type"] || "LIMIT",
391
- security_id: params["security_id"],
392
- quantity: params["quantity"] || 1,
393
- price: params["price"],
394
- target_price: params["target_price"],
395
- stop_loss_price: params["stop_loss_price"],
396
- trailing_jump: params["trailing_jump"] || 10
397
- )
398
- end
399
-
400
- def handle_cancel_order(params)
401
- DhanHQTradingTools.build_cancel_params(order_id: params["order_id"])
402
- end
403
-
404
- def handle_no_action
405
- { action: "no_action", message: "No action taken" }
406
- end
407
-
408
- def handle_unknown_action(action)
409
- { action: "unknown", error: "Unknown action: #{action}" }
410
- end
411
-
412
- def build_analysis_prompt(market_context:)
413
- <<~PROMPT
414
- Analyze the following market situation and decide the best trading action:
415
-
416
- Market Context:
417
- #{market_context}
418
-
419
- Available Actions (TRADING ONLY):
420
- - place_order: Build order parameters (requires: security_id as string, quantity, price, transaction_type, exchange_segment)
421
- - place_super_order: Build super order parameters with SL/TP (requires: security_id as string, quantity, price, target_price, stop_loss_price, exchange_segment)
422
- - cancel_order: Build cancel parameters (requires: order_id)
423
- - no_action: Take no action if market conditions are unclear or risky
424
-
425
- Important: security_id must be a STRING (e.g., "13" not 13). Valid exchange_segment values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
426
-
427
- CRITICAL: The parameters object must contain ONLY valid parameter values (strings, numbers, etc.).
428
- DO NOT include comments, instructions, or explanations in the parameters object.
429
- Parameters should be clean JSON values only.
430
-
431
- Decision Criteria:
432
- - Only take actions with confidence > 0.6
433
- - Consider risk management (use super orders for risky trades)
434
- - Ensure all required parameters are provided
435
- - Be conservative - prefer no_action if uncertain
436
-
437
- Respond with a JSON object containing:
438
- - action: one of the available trading actions
439
- - reasoning: why this action was chosen (put explanations here, NOT in parameters)
440
- - confidence: your confidence level (0-1)
441
- - parameters: object with ONLY required parameter values (no comments, no explanations)
442
- PROMPT
443
- end
444
- end
445
-
446
- def price_range_stats(price_ranges)
447
- return nil unless price_ranges.is_a?(Array) && price_ranges.any?
448
-
449
- {
450
- min: price_ranges.min.round(2),
451
- max: price_ranges.max.round(2),
452
- avg: (price_ranges.sum / price_ranges.length).round(2),
453
- count: price_ranges.length
454
- }
455
- end
456
-
457
- def build_expired_options_summary(stats)
458
- {
459
- data_points: stats[:data_points] || 0,
460
- avg_volume: stats[:avg_volume]&.round(2),
461
- avg_open_interest: stats[:avg_open_interest]&.round(2),
462
- avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
463
- price_range_stats: price_range_stats(stats[:price_ranges]),
464
- has_ohlc: stats[:has_ohlc],
465
- has_volume: stats[:has_volume],
466
- has_open_interest: stats[:has_open_interest],
467
- has_implied_volatility: stats[:has_implied_volatility]
468
- }
469
- end
470
-
471
- def build_option_chain_summary(chain_result)
472
- chain = chain_result[:result][:chain]
473
- underlying_price = chain_result[:result][:underlying_last_price]
474
-
475
- unless chain.is_a?(Hash)
476
- return [{ expiry: chain_result[:result][:expiry], chain_type: chain.class },
477
- underlying_price]
478
- end
479
-
480
- strike_prices = chain.keys.sort_by(&:to_f)
481
- first_strike_data = strike_prices.any? ? chain[strike_prices.first] : nil
482
- atm_strike = select_atm_strike(strike_prices, underlying_price)
483
- atm_data = atm_strike ? chain[atm_strike] : nil
484
- sample_greeks = build_sample_greeks(atm_data, atm_strike)
485
-
486
- summary = {
487
- expiry: chain_result[:result][:expiry],
488
- underlying_last_price: underlying_price,
489
- strikes_count: strike_prices.length,
490
- has_call_options: option_type_present?(first_strike_data, "ce"),
491
- has_put_options: option_type_present?(first_strike_data, "pe"),
492
- has_greeks: sample_greeks.any?,
493
- strike_range: strike_range_summary(strike_prices),
494
- sample_greeks: sample_greeks.any? ? sample_greeks : nil
495
- }
496
-
497
- [summary, underlying_price]
498
- end
499
-
500
- def select_atm_strike(strike_prices, underlying_price)
501
- return strike_prices.first unless underlying_price && strike_prices.any?
502
-
503
- strike_prices.min_by { |strike| (strike.to_f - underlying_price).abs }
504
- end
505
-
506
- def option_type_present?(strike_data, key)
507
- strike_data.is_a?(Hash) && (strike_data.key?(key) || strike_data.key?(key.to_sym))
508
- end
509
-
510
- def strike_range_summary(strike_prices)
511
- return nil if strike_prices.empty?
512
-
513
- {
514
- min: strike_prices.first,
515
- max: strike_prices.last,
516
- sample_strikes: strike_prices.first(5)
517
- }
518
- end
519
-
520
- def build_sample_greeks(atm_data, atm_strike)
521
- return {} unless atm_data.is_a?(Hash)
522
-
523
- sample = {}
524
- call_data = atm_data["ce"] || atm_data[:ce]
525
- put_data = atm_data["pe"] || atm_data[:pe]
526
-
527
- call_greeks = extract_greeks(call_data)
528
- sample[:call] = greeks_summary(call_greeks, call_data, atm_strike) if call_greeks
529
-
530
- put_greeks = extract_greeks(put_data)
531
- sample[:put] = greeks_summary(put_greeks, put_data, atm_strike) if put_greeks
532
-
533
- sample
534
- end
535
-
536
- def extract_greeks(option_data)
537
- return nil unless option_data.is_a?(Hash)
538
- return nil unless option_data.key?("greeks") || option_data.key?(:greeks)
539
-
540
- option_data["greeks"] || option_data[:greeks]
541
- end
542
-
543
- def greeks_summary(greeks, option_data, atm_strike)
544
- {
545
- strike: atm_strike,
546
- delta: greeks["delta"] || greeks[:delta],
547
- theta: greeks["theta"] || greeks[:theta],
548
- gamma: greeks["gamma"] || greeks[:gamma],
549
- vega: greeks["vega"] || greeks[:vega],
550
- iv: option_data["implied_volatility"] || option_data[:implied_volatility],
551
- oi: option_data["oi"] || option_data[:oi],
552
- last_price: option_data["last_price"] || option_data[:last_price]
553
- }
554
- end
555
-
556
- def format_score_breakdown(details)
557
- "Trend=#{details[:trend]}, RSI=#{details[:rsi]}, MACD=#{details[:macd]}, " \
558
- "Structure=#{details[:structure]}, Patterns=#{details[:patterns]}"
559
- end
560
-
561
- def format_option_setup_details(setup)
562
- iv = setup[:iv]&.round(2) || "N/A"
563
- oi = setup[:oi] || "N/A"
564
- volume = setup[:volume] || "N/A"
565
- "IV: #{iv}% | OI: #{oi} | Volume: #{volume}"
566
- end
567
-
568
- def handle_option_chain_result(chain_result)
569
- if chain_result[:result] && chain_result[:result][:chain]
570
- chain_summary, underlying_price = build_option_chain_summary(chain_result)
571
- puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
572
- puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
573
- puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
574
- elsif chain_result[:error]
575
- puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
576
- end
577
- end
578
-
579
- # Main execution
580
- if __FILE__ == $PROGRAM_NAME
581
- # Configure DhanHQ (must be done before using DhanHQ models)
582
- begin
583
- DhanHQ.configure_with_env
584
- puts "✅ DhanHQ configured"
585
- rescue StandardError => e
586
- puts "⚠️ DhanHQ configuration error: #{e.message}"
587
- puts " Make sure CLIENT_ID and ACCESS_TOKEN are set in ENV"
588
- puts " Continuing with mock data for demonstration..."
589
- end
590
-
591
- puts "=" * 60
592
- puts "DhanHQ Agent: Ollama (Reasoning) + DhanHQ (Data & Trading)"
593
- puts "=" * 60
594
- puts
595
-
596
- # Initialize Ollama client
597
- ollama_client = Ollama::Client.new
598
-
599
- # ============================================================
600
- # DATA AGENT EXAMPLES
601
- # ============================================================
602
- puts "─" * 60
603
- puts "DATA AGENT: Market Data Retrieval"
604
- puts "─" * 60
605
- puts
606
-
607
- data_agent = DataAgent.new(ollama_client: ollama_client)
608
-
609
- # Example 1: Analyze market and decide data action (using real data)
610
- puts "Example 1: Market Analysis & Data Decision (Real Data)"
611
- puts "─" * 60
612
-
613
- # Fetch real market data first
614
- puts "📊 Fetching real market data from DhanHQ..."
615
-
616
- market_data = {}
617
- begin
618
- # Get NIFTY data - using Instrument convenience method (uses symbol)
619
- # Note: Instrument.find expects symbol "NIFTY", not security_id
620
- # Rate limiting is handled automatically in DhanHQDataTools
621
- nifty_result = DhanHQDataTools.get_live_ltp(symbol: "NIFTY", exchange_segment: "IDX_I")
622
- sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
623
- if nifty_result.is_a?(Hash) && nifty_result[:result] && !nifty_result[:error]
624
- market_data[:nifty] = nifty_result[:result]
625
- ltp = nifty_result[:result][:ltp]
626
- if ltp && ltp != 0
627
- puts " ✅ NIFTY: LTP=#{ltp}"
628
- else
629
- puts " ⚠️ NIFTY: Data retrieved but LTP is null/empty (may be outside market hours)"
630
- puts " Result: #{JSON.pretty_generate(nifty_result[:result])}"
631
- end
632
- elsif nifty_result && nifty_result[:error]
633
- puts " ⚠️ NIFTY data error: #{nifty_result[:error]}"
634
- else
635
- puts " ⚠️ NIFTY: No data returned"
636
- end
637
- rescue StandardError => e
638
- puts " ⚠️ NIFTY data error: #{e.message}"
639
- end
640
-
641
- begin
642
- # Get RELIANCE data - using Instrument convenience method (uses symbol)
643
- # Note: Instrument.find expects symbol "RELIANCE", not security_id
644
- # Rate limiting is handled automatically in DhanHQDataTools
645
- reliance_result = DhanHQDataTools.get_live_ltp(symbol: "RELIANCE", exchange_segment: "NSE_EQ")
646
- sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
647
- if reliance_result.is_a?(Hash) && reliance_result[:result] && !reliance_result[:error]
648
- market_data[:reliance] = reliance_result[:result]
649
- ltp = reliance_result[:result][:ltp]
650
- if ltp && ltp != 0
651
- puts " ✅ RELIANCE: LTP=#{ltp}"
652
- else
653
- puts " ⚠️ RELIANCE: Data retrieved but LTP is null/empty (may be outside market hours)"
654
- puts " Result: #{JSON.pretty_generate(reliance_result[:result])}"
655
- end
656
- elsif reliance_result && reliance_result[:error]
657
- puts " ⚠️ RELIANCE data error: #{reliance_result[:error]}"
658
- else
659
- puts " ⚠️ RELIANCE: No data returned"
660
- end
661
- rescue StandardError => e
662
- puts " ⚠️ RELIANCE data error: #{e.message}"
663
- end
664
-
665
- # NOTE: Positions and holdings are not part of the 6 Data APIs, but available via DhanHQ gem
666
- begin
667
- positions_list = DhanHQ::Models::Position.all
668
- positions_data = positions_list.map do |pos|
669
- {
670
- trading_symbol: pos.trading_symbol,
671
- quantity: pos.net_qty,
672
- average_price: pos.buy_avg,
673
- exchange_segment: pos.exchange_segment,
674
- security_id: pos.security_id,
675
- pnl: pos.realized_profit
676
- }
677
- end
678
- market_data[:positions] = positions_data
679
- puts " ✅ Positions: #{positions_data.length} active"
680
-
681
- if positions_data.any?
682
- positions_data.each do |pos|
683
- puts " - #{pos[:trading_symbol]}: Qty #{pos[:quantity]} @ ₹#{pos[:average_price]}"
684
- end
685
- end
686
- rescue StandardError => e
687
- puts " ⚠️ Positions error: #{e.message}"
688
- market_data[:positions] = []
689
- end
690
-
691
- puts
692
-
693
- # Build market context from real data
694
- market_context = build_market_context_from_data(market_data)
695
-
696
- puts "Market Context (from real data):"
697
- puts market_context
698
- puts
699
-
700
- begin
701
- puts "🤔 Analyzing market with Ollama..."
702
- decision = data_agent.analyze_and_decide(market_context: market_context)
703
-
704
- puts "\n📋 Decision:"
705
- if decision.is_a?(Hash)
706
- puts " Action: #{decision['action'] || 'N/A'}"
707
- puts " Reasoning: #{decision['reasoning'] || 'N/A'}"
708
- if decision["confidence"]
709
- puts " Confidence: #{(decision['confidence'] * 100).round}%"
710
- else
711
- puts " Confidence: N/A"
712
- end
713
- puts " Parameters: #{JSON.pretty_generate(decision['parameters'] || {})}"
714
- else
715
- puts " ⚠️ Invalid decision returned: #{decision.inspect}"
716
- end
717
-
718
- if decision["action"] != "no_action"
719
- puts "\n⚡ Executing data retrieval..."
720
- result = data_agent.execute_decision(decision)
721
- puts " Result: #{JSON.pretty_generate(result)}"
722
- end
723
- rescue Ollama::Error => e
724
- puts "❌ Error: #{e.message}"
725
- end
726
-
727
- puts
728
- puts "─" * 60
729
- puts "Example 2: All Data APIs Demonstration"
730
- puts "─" * 60
731
- puts "Demonstrating all available DhanHQ Data APIs:"
732
- puts
733
-
734
- test_symbol = "RELIANCE" # RELIANCE symbol for Instrument.find
735
- test_exchange = "NSE_EQ"
736
-
737
- # 1. Market Quote (uses Instrument convenience method)
738
- puts "1️⃣ Market Quote API"
739
- begin
740
- result = DhanHQDataTools.get_market_quote(symbol: test_symbol, exchange_segment: test_exchange)
741
- if result[:result]
742
- puts " ✅ Market Quote retrieved"
743
- puts " 📊 Quote data: #{JSON.pretty_generate(result[:result][:quote])}"
744
- else
745
- puts " ⚠️ #{result[:error]}"
746
- end
747
- rescue StandardError => e
748
- puts " ❌ Error: #{e.message}"
749
- end
750
-
751
- puts
752
- sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
753
-
754
- # 2. Live Market Feed (LTP) (uses Instrument convenience method)
755
- puts "2️⃣ Live Market Feed API (LTP)"
756
- begin
757
- result = DhanHQDataTools.get_live_ltp(symbol: test_symbol, exchange_segment: test_exchange)
758
- if result[:result]
759
- puts " ✅ LTP retrieved"
760
- puts " 📊 LTP: #{result[:result][:ltp].inspect}"
761
- else
762
- puts " ⚠️ #{result[:error]}"
763
- end
764
- rescue StandardError => e
765
- puts " ❌ Error: #{e.message}"
766
- end
767
-
768
- puts
769
- sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
770
-
771
- # 3. Full Market Depth (uses Instrument convenience method)
772
- puts "3️⃣ Full Market Depth API"
773
- begin
774
- result = DhanHQDataTools.get_market_depth(symbol: test_symbol, exchange_segment: test_exchange)
775
- if result[:result]
776
- puts " ✅ Market Depth retrieved"
777
- puts " 📊 Buy depth: #{result[:result][:buy_depth]&.length || 0} levels"
778
- puts " 📊 Sell depth: #{result[:result][:sell_depth]&.length || 0} levels"
779
- puts " 📊 LTP: #{result[:result][:ltp]}"
780
- puts " 📊 Volume: #{result[:result][:volume]}"
781
- else
782
- puts " ⚠️ #{result[:error]}"
783
- end
784
- rescue StandardError => e
785
- puts " ❌ Error: #{e.message}"
786
- end
787
-
788
- puts
789
- sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
790
-
791
- # 4. Historical Data (uses symbol for Instrument.find)
792
- puts "4️⃣ Historical Data API"
793
- begin
794
- # Use recent dates (last 30 days) for better data availability
795
- to_date = Date.today.strftime("%Y-%m-%d")
796
- from_date = (Date.today - 30).strftime("%Y-%m-%d")
797
- result = DhanHQDataTools.get_historical_data(
798
- symbol: test_symbol,
799
- exchange_segment: test_exchange,
800
- from_date: from_date,
801
- to_date: to_date
802
- )
803
- if result[:result]
804
- puts " ✅ Historical data retrieved"
805
- puts " 📊 Type: #{result[:type]}"
806
- puts " 📊 Records: #{result[:result][:count]}"
807
- if result[:result][:count].zero?
808
- puts " ⚠️ No data found for date range #{from_date} to #{to_date}"
809
- puts " (This may be normal if market was closed or data unavailable)"
810
- end
811
- else
812
- puts " ⚠️ #{result[:error]}"
813
- end
814
- rescue StandardError => e
815
- puts " ❌ Error: #{e.message}"
816
- end
817
-
818
- puts
819
- sleep(0.5) # Small delay for Instrument APIs
820
-
821
- # 5. Expired Options Data (uses symbol for Instrument.find)
822
- puts "5️⃣ Expired Options Data API"
823
- begin
824
- # Use NSE_FNO for options
825
- # Note: For NIFTY index options, use security_id=13 directly (NIFTY is in IDX_I, not NSE_FNO)
826
- # Try with NIFTY which typically has options
827
- result = DhanHQDataTools.get_expired_options_data(
828
- security_id: "13", # NIFTY security_id (use directly since symbol lookup might fail in NSE_FNO)
829
- exchange_segment: "NSE_FNO",
830
- expiry_date: (Date.today - 7).strftime("%Y-%m-%d"), # Use recent expired date
831
- instrument: "OPTIDX", # Index options
832
- expiry_flag: "MONTH",
833
- expiry_code: 1, # Use 1 (near month) as default
834
- strike: "ATM",
835
- drv_option_type: "CALL",
836
- interval: "1"
837
- )
838
- if result[:result]
839
- puts " ✅ Expired options data retrieved"
840
- puts " 📊 Expiry: #{result[:result][:expiry_date]}"
841
- # Show concise summary of expired options data instead of full data (can be very large)
842
- if result[:result][:summary_stats]
843
- stats = result[:result][:summary_stats]
844
- concise_summary = build_expired_options_summary(stats)
845
- puts " 📊 Data summary: #{JSON.pretty_generate(concise_summary)}"
846
- else
847
- puts " 📊 Data available but summary stats not found"
848
- end
849
- else
850
- puts " ⚠️ #{result[:error]}"
851
- puts " (Note: Options may require specific symbol format or may not exist for this instrument)"
852
- end
853
- rescue StandardError => e
854
- puts " ❌ Error: #{e.message}"
855
- end
856
-
857
- puts
858
- sleep(0.5) # Small delay for Instrument APIs
859
-
860
- # 6. Option Chain (uses symbol for Instrument.find)
861
- puts "6️⃣ Option Chain API"
862
- begin
863
- # NOTE: Options symbols may need different format
864
- # Try with NIFTY which typically has options
865
- # First, get the list of available expiries using get_expiry_list
866
- expiry_list_result = DhanHQDataTools.get_expiry_list(
867
- symbol: "NIFTY", # NIFTY typically has options, RELIANCE might not
868
- exchange_segment: "IDX_I"
869
- )
870
- if expiry_list_result[:result] && expiry_list_result[:result][:expiries]
871
- expiries = expiry_list_result[:result][:expiries]
872
- puts " ✅ Available expiries: #{expiry_list_result[:result][:count]}"
873
- puts " 📊 First few expiries: #{expiries.first(3).inspect}" if expiries.is_a?(Array) && !expiries.empty?
874
-
875
- # Get the actual option chain for the next/upcoming expiry
876
- next_expiry = expiries.is_a?(Array) && !expiries.empty? ? expiries.first : nil
877
- if next_expiry
878
- puts " 📊 Fetching option chain for next expiry: #{next_expiry}"
879
- # For NIFTY index options, use IDX_I as underlying_seg, not NSE_FNO
880
- chain_result = DhanHQDataTools.get_option_chain(
881
- symbol: "NIFTY",
882
- exchange_segment: "IDX_I", # Use IDX_I for index options underlying
883
- expiry: next_expiry
884
- )
885
- handle_option_chain_result(chain_result)
886
- end
887
- elsif expiry_list_result[:error]
888
- puts " ⚠️ #{expiry_list_result[:error]}"
889
- puts " (Note: Options may require specific symbol format or may not exist for this instrument)"
890
- end
891
- rescue StandardError => e
892
- puts " ❌ Error: #{e.message}"
893
- end
894
-
895
- puts
896
- puts "=" * 60
897
- puts "TRADING AGENT: Order Parameter Building"
898
- puts "=" * 60
899
- puts
900
-
901
- # ============================================================
902
- # TRADING AGENT EXAMPLES
903
- # ============================================================
904
- config = Ollama::Config.new
905
- config.timeout = 60
906
- trading_ollama_client = Ollama::Client.new(config: config)
907
- trading_agent = TradingAgent.new(ollama_client: trading_ollama_client)
908
-
909
- # Example 1: Simple buy order
910
- puts "Example 1: Simple Buy Order"
911
- puts "─" * 60
912
-
913
- market_context = <<~CONTEXT
914
- RELIANCE is showing strong momentum.
915
- Current LTP: 2,850
916
- Entry price: 2,850
917
- Quantity: 100 shares
918
- Use regular order. security_id="1333", exchange_segment="NSE_EQ"
919
- CONTEXT
920
-
921
- puts "Market Context:"
922
- puts market_context
923
- puts
924
-
925
- begin
926
- puts "🤔 Analyzing with Ollama..."
927
- decision = trading_agent.analyze_and_decide(market_context: market_context)
928
-
929
- puts "\n📋 Decision:"
930
- if decision.is_a?(Hash)
931
- puts " Action: #{decision['action'] || 'N/A'}"
932
- puts " Reasoning: #{decision['reasoning'] || 'N/A'}"
933
- puts " Confidence: #{(decision['confidence'] * 100).round}%" if decision["confidence"]
934
- puts " Parameters: #{JSON.pretty_generate(decision['parameters'] || {})}"
935
- end
936
-
937
- if decision["action"] != "no_action"
938
- puts "\n⚡ Building order parameters (order not placed)..."
939
- result = trading_agent.execute_decision(decision)
940
- puts " Result: #{JSON.pretty_generate(result)}"
941
- if result.is_a?(Hash) && result[:order_params]
942
- puts "\n 📝 Order Parameters Ready:"
943
- puts " #{JSON.pretty_generate(result[:order_params])}"
944
- puts " 💡 To place order: DhanHQ::Models::Order.new(result[:order_params]).save"
945
- end
946
- end
947
- rescue Ollama::TimeoutError => e
948
- puts "⏱️ Timeout: #{e.message}"
949
- rescue Ollama::Error => e
950
- puts "❌ Error: #{e.message}"
951
- end
952
-
953
- puts
954
- puts "=" * 60
955
- puts "DhanHQ Agent Summary:"
956
- puts " ✅ Ollama: Reasoning & Decision Making"
957
- puts " ✅ DhanHQ: Data Retrieval & Order Building"
958
- puts " ✅ Data APIs: Market Quote, Live Market Feed, Full Market Depth, " \
959
- "Historical Data, Expired Options Data, Option Chain"
960
- puts " ✅ Trading Tools: Order parameters, Super order parameters, Cancel parameters"
961
- puts " ✅ Instrument Convenience Methods: ltp, ohlc, quote, daily, intraday, expiry_list, option_chain"
962
- puts "=" * 60
963
- end
964
-