ollama-client 0.2.0
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 +7 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +36 -0
- data/LICENSE.txt +21 -0
- data/PRODUCTION_FIXES.md +172 -0
- data/README.md +690 -0
- data/Rakefile +12 -0
- data/TESTING.md +286 -0
- data/examples/advanced_complex_schemas.rb +363 -0
- data/examples/advanced_edge_cases.rb +241 -0
- data/examples/advanced_error_handling.rb +200 -0
- data/examples/advanced_multi_step_agent.rb +258 -0
- data/examples/advanced_performance_testing.rb +186 -0
- data/examples/complete_workflow.rb +235 -0
- data/examples/dhanhq_agent.rb +752 -0
- data/examples/dhanhq_tools.rb +563 -0
- data/examples/structured_outputs_chat.rb +72 -0
- data/examples/tool_calling_pattern.rb +266 -0
- data/exe/ollama-client +4 -0
- data/lib/ollama/agent/executor.rb +157 -0
- data/lib/ollama/agent/messages.rb +31 -0
- data/lib/ollama/agent/planner.rb +47 -0
- data/lib/ollama/client.rb +775 -0
- data/lib/ollama/config.rb +29 -0
- data/lib/ollama/errors.rb +54 -0
- data/lib/ollama/schema_validator.rb +79 -0
- data/lib/ollama/schemas/base.json +5 -0
- data/lib/ollama/streaming_observer.rb +22 -0
- data/lib/ollama/version.rb +5 -0
- data/lib/ollama_client.rb +46 -0
- data/sig/ollama/client.rbs +6 -0
- metadata +108 -0
|
@@ -0,0 +1,752 @@
|
|
|
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 "dhan_hq"
|
|
11
|
+
require_relative "../lib/ollama_client"
|
|
12
|
+
require_relative "dhanhq_tools"
|
|
13
|
+
|
|
14
|
+
# Debug logging helper
|
|
15
|
+
def debug_log(location, message, data = {}, hypothesis_id = nil)
|
|
16
|
+
log_entry = {
|
|
17
|
+
sessionId: "debug-session",
|
|
18
|
+
runId: "run1",
|
|
19
|
+
hypothesisId: hypothesis_id,
|
|
20
|
+
location: location,
|
|
21
|
+
message: message,
|
|
22
|
+
data: data,
|
|
23
|
+
timestamp: Time.now.to_f * 1000
|
|
24
|
+
}
|
|
25
|
+
File.open("/home/nemesis/project/ollama-client/.cursor/debug.log", "a") do |f|
|
|
26
|
+
f.puts(log_entry.to_json)
|
|
27
|
+
end
|
|
28
|
+
rescue StandardError
|
|
29
|
+
# Ignore logging errors
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Helper to build market context from data
|
|
33
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
34
|
+
def build_market_context_from_data(market_data)
|
|
35
|
+
context_parts = []
|
|
36
|
+
|
|
37
|
+
if market_data[:nifty]
|
|
38
|
+
ltp = market_data[:nifty][:ltp]
|
|
39
|
+
change = market_data[:nifty][:change_percent]
|
|
40
|
+
context_parts << if ltp && ltp != 0
|
|
41
|
+
"NIFTY is trading at #{ltp} (#{change || 'unknown'}% change)"
|
|
42
|
+
else
|
|
43
|
+
"NIFTY data retrieved but LTP is not available (may be outside market hours)"
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
context_parts << "NIFTY data not available"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if market_data[:reliance]
|
|
50
|
+
ltp = market_data[:reliance][:ltp]
|
|
51
|
+
change = market_data[:reliance][:change_percent]
|
|
52
|
+
volume = market_data[:reliance][:volume]
|
|
53
|
+
context_parts << if ltp && ltp != 0
|
|
54
|
+
"RELIANCE is at #{ltp} (#{change || 'unknown'}% change, Volume: #{volume || 'N/A'})"
|
|
55
|
+
else
|
|
56
|
+
"RELIANCE data retrieved but LTP is not available (may be outside market hours)"
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
context_parts << "RELIANCE data not available"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if market_data[:positions] && !market_data[:positions].empty?
|
|
63
|
+
context_parts << "Current positions: #{market_data[:positions].length} active"
|
|
64
|
+
market_data[:positions].each do |pos|
|
|
65
|
+
context_parts << " - #{pos[:trading_symbol]}: #{pos[:quantity]} @ #{pos[:average_price]}"
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
context_parts << "Current positions: None"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context_parts.join("\n")
|
|
72
|
+
end
|
|
73
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
74
|
+
|
|
75
|
+
# Data-focused Agent using Ollama for reasoning
|
|
76
|
+
class DataAgent
|
|
77
|
+
def initialize(ollama_client:)
|
|
78
|
+
@ollama_client = ollama_client
|
|
79
|
+
@decision_schema = {
|
|
80
|
+
"type" => "object",
|
|
81
|
+
"required" => ["action", "reasoning", "confidence"],
|
|
82
|
+
"properties" => {
|
|
83
|
+
"action" => {
|
|
84
|
+
"type" => "string",
|
|
85
|
+
"enum" => ["get_market_quote", "get_live_ltp", "get_market_depth", "get_historical_data",
|
|
86
|
+
"get_expired_options_data", "get_option_chain", "no_action"]
|
|
87
|
+
},
|
|
88
|
+
"reasoning" => {
|
|
89
|
+
"type" => "string",
|
|
90
|
+
"description" => "Why this action was chosen"
|
|
91
|
+
},
|
|
92
|
+
"confidence" => {
|
|
93
|
+
"type" => "number",
|
|
94
|
+
"minimum" => 0,
|
|
95
|
+
"maximum" => 1,
|
|
96
|
+
"description" => "Confidence in this decision"
|
|
97
|
+
},
|
|
98
|
+
"parameters" => {
|
|
99
|
+
"type" => "object",
|
|
100
|
+
"description" => "Parameters for the action (symbol, exchange_segment, etc.)"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def analyze_and_decide(market_context:)
|
|
107
|
+
prompt = build_analysis_prompt(market_context: market_context)
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
decision = @ollama_client.generate(
|
|
111
|
+
prompt: prompt,
|
|
112
|
+
schema: @decision_schema
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Validate confidence threshold
|
|
116
|
+
return { action: "no_action", reason: "invalid_decision" } unless decision.is_a?(Hash) && decision["confidence"]
|
|
117
|
+
|
|
118
|
+
if decision["confidence"] < 0.6
|
|
119
|
+
puts "ā ļø Low confidence (#{(decision["confidence"] * 100).round}%) - skipping action"
|
|
120
|
+
return { action: "no_action", reason: "low_confidence" }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
decision
|
|
124
|
+
rescue Ollama::Error => e
|
|
125
|
+
puts "ā Ollama error: #{e.message}"
|
|
126
|
+
{ action: "no_action", reason: "error", error: e.message }
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
puts "ā Unexpected error: #{e.message}"
|
|
129
|
+
{ action: "no_action", reason: "error", error: e.message }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
134
|
+
def execute_decision(decision)
|
|
135
|
+
action = decision["action"]
|
|
136
|
+
params = decision["parameters"] || {}
|
|
137
|
+
|
|
138
|
+
case action
|
|
139
|
+
when "get_market_quote"
|
|
140
|
+
if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
141
|
+
{ action: "get_market_quote", error: "Either symbol or security_id is required", params: params }
|
|
142
|
+
else
|
|
143
|
+
DhanHQDataTools.get_market_quote(
|
|
144
|
+
symbol: params["symbol"],
|
|
145
|
+
security_id: params["security_id"],
|
|
146
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ"
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
when "get_live_ltp"
|
|
151
|
+
if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
152
|
+
{ action: "get_live_ltp", error: "Either symbol or security_id is required", params: params }
|
|
153
|
+
else
|
|
154
|
+
DhanHQDataTools.get_live_ltp(
|
|
155
|
+
symbol: params["symbol"],
|
|
156
|
+
security_id: params["security_id"],
|
|
157
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ"
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
when "get_market_depth"
|
|
162
|
+
if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
163
|
+
{ action: "get_market_depth", error: "Either symbol or security_id is required", params: params }
|
|
164
|
+
else
|
|
165
|
+
DhanHQDataTools.get_market_depth(
|
|
166
|
+
symbol: params["symbol"],
|
|
167
|
+
security_id: params["security_id"],
|
|
168
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ"
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
when "get_historical_data"
|
|
173
|
+
if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
174
|
+
{ action: "get_historical_data", error: "Either symbol or security_id is required", params: params }
|
|
175
|
+
else
|
|
176
|
+
DhanHQDataTools.get_historical_data(
|
|
177
|
+
symbol: params["symbol"],
|
|
178
|
+
security_id: params["security_id"],
|
|
179
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ",
|
|
180
|
+
from_date: params["from_date"],
|
|
181
|
+
to_date: params["to_date"],
|
|
182
|
+
interval: params["interval"],
|
|
183
|
+
expiry_code: params["expiry_code"]
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
when "get_option_chain"
|
|
188
|
+
if params["symbol"].nil? && (params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
189
|
+
{ action: "get_option_chain", error: "Either symbol or security_id is required", params: params }
|
|
190
|
+
else
|
|
191
|
+
DhanHQDataTools.get_option_chain(
|
|
192
|
+
symbol: params["symbol"],
|
|
193
|
+
security_id: params["security_id"],
|
|
194
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ",
|
|
195
|
+
expiry: params["expiry"]
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
when "get_expired_options_data"
|
|
200
|
+
symbol_or_id_missing = params["symbol"].nil? &&
|
|
201
|
+
(params["security_id"].nil? || params["security_id"].to_s.empty?)
|
|
202
|
+
if symbol_or_id_missing || params["expiry_date"].nil?
|
|
203
|
+
{
|
|
204
|
+
action: "get_expired_options_data",
|
|
205
|
+
error: "Either symbol or security_id, and expiry_date are required",
|
|
206
|
+
params: params
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
DhanHQDataTools.get_expired_options_data(
|
|
210
|
+
symbol: params["symbol"],
|
|
211
|
+
security_id: params["security_id"],
|
|
212
|
+
exchange_segment: params["exchange_segment"] || "NSE_FNO",
|
|
213
|
+
expiry_date: params["expiry_date"],
|
|
214
|
+
expiry_code: params["expiry_code"]
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
when "no_action"
|
|
219
|
+
{ action: "no_action", message: "No action taken" }
|
|
220
|
+
|
|
221
|
+
else
|
|
222
|
+
{ action: "unknown", error: "Unknown action: #{action}" }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def build_analysis_prompt(market_context:)
|
|
230
|
+
<<~PROMPT
|
|
231
|
+
Analyze the following market situation and decide the best data retrieval action:
|
|
232
|
+
|
|
233
|
+
Market Context:
|
|
234
|
+
#{market_context}
|
|
235
|
+
|
|
236
|
+
Available Actions (DATA ONLY - NO TRADING):
|
|
237
|
+
- get_market_quote: Get market quote using Instrument.quote convenience method (requires: symbol OR security_id, exchange_segment)
|
|
238
|
+
- get_live_ltp: Get live last traded price using Instrument.ltp convenience method (requires: symbol OR security_id, exchange_segment)
|
|
239
|
+
- get_market_depth: Get full market depth (bid/ask levels) using Instrument.quote convenience method (requires: symbol OR security_id, exchange_segment)
|
|
240
|
+
- get_historical_data: Get historical data using Instrument.daily/intraday convenience methods (requires: symbol OR security_id, exchange_segment, from_date, to_date, optional: interval, expiry_code)
|
|
241
|
+
- get_expired_options_data: Get expired options historical data using Instrument.daily convenience method (requires: symbol OR security_id, exchange_segment, expiry_date, optional: expiry_code)
|
|
242
|
+
- get_option_chain: Get option chain using Instrument.expiry_list/option_chain convenience methods (requires: symbol OR security_id, exchange_segment, optional: expiry)
|
|
243
|
+
- no_action: Take no action if unclear what data is needed
|
|
244
|
+
|
|
245
|
+
Important:
|
|
246
|
+
- All APIs use Instrument.find() which expects SYMBOL (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
247
|
+
- Instrument convenience methods automatically use the instrument's security_id, exchange_segment, and instrument attributes
|
|
248
|
+
- Use symbol when possible for better compatibility
|
|
249
|
+
Examples: NIFTY=symbol "NIFTY", exchange_segment "IDX_I"; RELIANCE=symbol "RELIANCE", exchange_segment "NSE_EQ"
|
|
250
|
+
Valid exchange_segments: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
251
|
+
|
|
252
|
+
Decision Criteria:
|
|
253
|
+
- Only take actions with confidence > 0.6
|
|
254
|
+
- Focus on data retrieval, not trading decisions
|
|
255
|
+
- Provide all required parameters for the chosen action
|
|
256
|
+
|
|
257
|
+
Respond with a JSON object containing:
|
|
258
|
+
- action: one of the available actions
|
|
259
|
+
- reasoning: why this action was chosen
|
|
260
|
+
- confidence: your confidence level (0-1)
|
|
261
|
+
- parameters: object with required parameters for the action
|
|
262
|
+
PROMPT
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Trading-focused Agent using Ollama for reasoning
|
|
267
|
+
class TradingAgent
|
|
268
|
+
def initialize(ollama_client:)
|
|
269
|
+
@ollama_client = ollama_client
|
|
270
|
+
@decision_schema = {
|
|
271
|
+
"type" => "object",
|
|
272
|
+
"required" => ["action", "reasoning", "confidence"],
|
|
273
|
+
"properties" => {
|
|
274
|
+
"action" => {
|
|
275
|
+
"type" => "string",
|
|
276
|
+
"enum" => ["place_order", "place_super_order", "cancel_order", "no_action"]
|
|
277
|
+
},
|
|
278
|
+
"reasoning" => {
|
|
279
|
+
"type" => "string",
|
|
280
|
+
"description" => "Why this action was chosen"
|
|
281
|
+
},
|
|
282
|
+
"confidence" => {
|
|
283
|
+
"type" => "number",
|
|
284
|
+
"minimum" => 0,
|
|
285
|
+
"maximum" => 1,
|
|
286
|
+
"description" => "Confidence in this decision"
|
|
287
|
+
},
|
|
288
|
+
"parameters" => {
|
|
289
|
+
"type" => "object",
|
|
290
|
+
"description" => "Parameters for the action (security_id, quantity, price, etc.)"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def analyze_and_decide(market_context:)
|
|
297
|
+
prompt = build_analysis_prompt(market_context: market_context)
|
|
298
|
+
|
|
299
|
+
begin
|
|
300
|
+
decision = @ollama_client.generate(
|
|
301
|
+
prompt: prompt,
|
|
302
|
+
schema: @decision_schema
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Validate confidence threshold
|
|
306
|
+
return { action: "no_action", reason: "invalid_decision" } unless decision.is_a?(Hash) && decision["confidence"]
|
|
307
|
+
|
|
308
|
+
if decision["confidence"] < 0.6
|
|
309
|
+
puts "ā ļø Low confidence (#{(decision["confidence"] * 100).round}%) - skipping action"
|
|
310
|
+
return { action: "no_action", reason: "low_confidence" }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
decision
|
|
314
|
+
rescue Ollama::Error => e
|
|
315
|
+
puts "ā Ollama error: #{e.message}"
|
|
316
|
+
{ action: "no_action", reason: "error", error: e.message }
|
|
317
|
+
rescue StandardError => e
|
|
318
|
+
puts "ā Unexpected error: #{e.message}"
|
|
319
|
+
{ action: "no_action", reason: "error", error: e.message }
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def execute_decision(decision)
|
|
324
|
+
action = decision["action"]
|
|
325
|
+
params = decision["parameters"] || {}
|
|
326
|
+
|
|
327
|
+
case action
|
|
328
|
+
when "place_order"
|
|
329
|
+
handle_place_order(params)
|
|
330
|
+
when "place_super_order"
|
|
331
|
+
handle_place_super_order(params)
|
|
332
|
+
when "cancel_order"
|
|
333
|
+
handle_cancel_order(params)
|
|
334
|
+
when "no_action"
|
|
335
|
+
handle_no_action
|
|
336
|
+
else
|
|
337
|
+
handle_unknown_action(action)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def handle_place_order(params)
|
|
344
|
+
DhanHQTradingTools.build_order_params(
|
|
345
|
+
transaction_type: params["transaction_type"] || "BUY",
|
|
346
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ",
|
|
347
|
+
product_type: params["product_type"] || "MARGIN",
|
|
348
|
+
order_type: params["order_type"] || "LIMIT",
|
|
349
|
+
security_id: params["security_id"],
|
|
350
|
+
quantity: params["quantity"] || 1,
|
|
351
|
+
price: params["price"]
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def handle_place_super_order(params)
|
|
356
|
+
DhanHQTradingTools.build_super_order_params(
|
|
357
|
+
transaction_type: params["transaction_type"] || "BUY",
|
|
358
|
+
exchange_segment: params["exchange_segment"] || "NSE_EQ",
|
|
359
|
+
product_type: params["product_type"] || "MARGIN",
|
|
360
|
+
order_type: params["order_type"] || "LIMIT",
|
|
361
|
+
security_id: params["security_id"],
|
|
362
|
+
quantity: params["quantity"] || 1,
|
|
363
|
+
price: params["price"],
|
|
364
|
+
target_price: params["target_price"],
|
|
365
|
+
stop_loss_price: params["stop_loss_price"],
|
|
366
|
+
trailing_jump: params["trailing_jump"] || 10
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def handle_cancel_order(params)
|
|
371
|
+
DhanHQTradingTools.build_cancel_params(order_id: params["order_id"])
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def handle_no_action
|
|
375
|
+
{ action: "no_action", message: "No action taken" }
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def handle_unknown_action(action)
|
|
379
|
+
{ action: "unknown", error: "Unknown action: #{action}" }
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def build_analysis_prompt(market_context:)
|
|
383
|
+
<<~PROMPT
|
|
384
|
+
Analyze the following market situation and decide the best trading action:
|
|
385
|
+
|
|
386
|
+
Market Context:
|
|
387
|
+
#{market_context}
|
|
388
|
+
|
|
389
|
+
Available Actions (TRADING ONLY):
|
|
390
|
+
- place_order: Build order parameters (requires: security_id as string, quantity, price, transaction_type, exchange_segment)
|
|
391
|
+
- place_super_order: Build super order parameters with SL/TP (requires: security_id as string, quantity, price, target_price, stop_loss_price, exchange_segment)
|
|
392
|
+
- cancel_order: Build cancel parameters (requires: order_id)
|
|
393
|
+
- no_action: Take no action if market conditions are unclear or risky
|
|
394
|
+
|
|
395
|
+
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
|
|
396
|
+
|
|
397
|
+
Decision Criteria:
|
|
398
|
+
- Only take actions with confidence > 0.6
|
|
399
|
+
- Consider risk management (use super orders for risky trades)
|
|
400
|
+
- Ensure all required parameters are provided
|
|
401
|
+
- Be conservative - prefer no_action if uncertain
|
|
402
|
+
|
|
403
|
+
Respond with a JSON object containing:
|
|
404
|
+
- action: one of the available trading actions
|
|
405
|
+
- reasoning: why this action was chosen
|
|
406
|
+
- confidence: your confidence level (0-1)
|
|
407
|
+
- parameters: object with required parameters for the action
|
|
408
|
+
PROMPT
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Main execution
|
|
413
|
+
if __FILE__ == $PROGRAM_NAME
|
|
414
|
+
# Configure DhanHQ (must be done before using DhanHQ models)
|
|
415
|
+
begin
|
|
416
|
+
DhanHQ.configure_with_env
|
|
417
|
+
puts "ā
DhanHQ configured"
|
|
418
|
+
rescue StandardError => e
|
|
419
|
+
puts "ā ļø DhanHQ configuration error: #{e.message}"
|
|
420
|
+
puts " Make sure CLIENT_ID and ACCESS_TOKEN are set in ENV"
|
|
421
|
+
puts " Continuing with mock data for demonstration..."
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
puts "=" * 60
|
|
425
|
+
puts "DhanHQ Agent: Ollama (Reasoning) + DhanHQ (Data & Trading)"
|
|
426
|
+
puts "=" * 60
|
|
427
|
+
puts
|
|
428
|
+
|
|
429
|
+
# Initialize Ollama client
|
|
430
|
+
ollama_client = Ollama::Client.new
|
|
431
|
+
|
|
432
|
+
# ============================================================
|
|
433
|
+
# DATA AGENT EXAMPLES
|
|
434
|
+
# ============================================================
|
|
435
|
+
puts "ā" * 60
|
|
436
|
+
puts "DATA AGENT: Market Data Retrieval"
|
|
437
|
+
puts "ā" * 60
|
|
438
|
+
puts
|
|
439
|
+
|
|
440
|
+
data_agent = DataAgent.new(ollama_client: ollama_client)
|
|
441
|
+
|
|
442
|
+
# Example 1: Analyze market and decide data action (using real data)
|
|
443
|
+
puts "Example 1: Market Analysis & Data Decision (Real Data)"
|
|
444
|
+
puts "ā" * 60
|
|
445
|
+
|
|
446
|
+
# Fetch real market data first
|
|
447
|
+
puts "š Fetching real market data from DhanHQ..."
|
|
448
|
+
|
|
449
|
+
market_data = {}
|
|
450
|
+
begin
|
|
451
|
+
# Get NIFTY data - using Instrument convenience method (uses symbol)
|
|
452
|
+
# Note: Instrument.find expects symbol "NIFTY", not security_id
|
|
453
|
+
# Rate limiting is handled automatically in DhanHQDataTools
|
|
454
|
+
nifty_result = DhanHQDataTools.get_live_ltp(symbol: "NIFTY", exchange_segment: "IDX_I")
|
|
455
|
+
sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
|
|
456
|
+
if nifty_result.is_a?(Hash) && nifty_result[:result] && !nifty_result[:error]
|
|
457
|
+
market_data[:nifty] = nifty_result[:result]
|
|
458
|
+
ltp = nifty_result[:result][:ltp]
|
|
459
|
+
if ltp && ltp != 0
|
|
460
|
+
puts " ā
NIFTY: LTP=#{ltp}"
|
|
461
|
+
else
|
|
462
|
+
puts " ā ļø NIFTY: Data retrieved but LTP is null/empty (may be outside market hours)"
|
|
463
|
+
puts " Result: #{nifty_result[:result].inspect[0..200]}"
|
|
464
|
+
end
|
|
465
|
+
elsif nifty_result && nifty_result[:error]
|
|
466
|
+
puts " ā ļø NIFTY data error: #{nifty_result[:error]}"
|
|
467
|
+
else
|
|
468
|
+
puts " ā ļø NIFTY: No data returned"
|
|
469
|
+
end
|
|
470
|
+
rescue StandardError => e
|
|
471
|
+
puts " ā ļø NIFTY data error: #{e.message}"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
begin
|
|
475
|
+
# Get RELIANCE data - using Instrument convenience method (uses symbol)
|
|
476
|
+
# Note: Instrument.find expects symbol "RELIANCE", not security_id
|
|
477
|
+
# Rate limiting is handled automatically in DhanHQDataTools
|
|
478
|
+
reliance_result = DhanHQDataTools.get_live_ltp(symbol: "RELIANCE", exchange_segment: "NSE_EQ")
|
|
479
|
+
sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
|
|
480
|
+
if reliance_result.is_a?(Hash) && reliance_result[:result] && !reliance_result[:error]
|
|
481
|
+
market_data[:reliance] = reliance_result[:result]
|
|
482
|
+
ltp = reliance_result[:result][:ltp]
|
|
483
|
+
if ltp && ltp != 0
|
|
484
|
+
puts " ā
RELIANCE: LTP=#{ltp}"
|
|
485
|
+
else
|
|
486
|
+
puts " ā ļø RELIANCE: Data retrieved but LTP is null/empty (may be outside market hours)"
|
|
487
|
+
puts " Result: #{reliance_result[:result].inspect[0..200]}"
|
|
488
|
+
end
|
|
489
|
+
elsif reliance_result && reliance_result[:error]
|
|
490
|
+
puts " ā ļø RELIANCE data error: #{reliance_result[:error]}"
|
|
491
|
+
else
|
|
492
|
+
puts " ā ļø RELIANCE: No data returned"
|
|
493
|
+
end
|
|
494
|
+
rescue StandardError => e
|
|
495
|
+
puts " ā ļø RELIANCE data error: #{e.message}"
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# NOTE: Positions and holdings are not part of the 6 Data APIs
|
|
499
|
+
begin
|
|
500
|
+
positions_result = { action: "check_positions", result: { positions: [], count: 0 },
|
|
501
|
+
note: "Positions API not available in Data Tools" }
|
|
502
|
+
if positions_result[:result]
|
|
503
|
+
market_data[:positions] = positions_result[:result][:positions] || []
|
|
504
|
+
puts " ā
Positions: #{positions_result[:result][:count] || 0} active"
|
|
505
|
+
else
|
|
506
|
+
puts " ā
Positions: 0 active (Positions API not in Data Tools)"
|
|
507
|
+
market_data[:positions] = []
|
|
508
|
+
end
|
|
509
|
+
rescue StandardError => e
|
|
510
|
+
puts " ā ļø Positions error: #{e.message}"
|
|
511
|
+
market_data[:positions] = []
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
puts
|
|
515
|
+
|
|
516
|
+
# Build market context from real data
|
|
517
|
+
market_context = build_market_context_from_data(market_data)
|
|
518
|
+
|
|
519
|
+
puts "Market Context (from real data):"
|
|
520
|
+
puts market_context
|
|
521
|
+
puts
|
|
522
|
+
|
|
523
|
+
begin
|
|
524
|
+
puts "š¤ Analyzing market with Ollama..."
|
|
525
|
+
decision = data_agent.analyze_and_decide(market_context: market_context)
|
|
526
|
+
|
|
527
|
+
puts "\nš Decision:"
|
|
528
|
+
if decision.is_a?(Hash)
|
|
529
|
+
puts " Action: #{decision['action'] || 'N/A'}"
|
|
530
|
+
puts " Reasoning: #{decision['reasoning'] || 'N/A'}"
|
|
531
|
+
if decision["confidence"]
|
|
532
|
+
puts " Confidence: #{(decision['confidence'] * 100).round}%"
|
|
533
|
+
else
|
|
534
|
+
puts " Confidence: N/A"
|
|
535
|
+
end
|
|
536
|
+
puts " Parameters: #{JSON.pretty_generate(decision['parameters'] || {})}"
|
|
537
|
+
else
|
|
538
|
+
puts " ā ļø Invalid decision returned: #{decision.inspect}"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
if decision["action"] != "no_action"
|
|
542
|
+
puts "\nā” Executing data retrieval..."
|
|
543
|
+
result = data_agent.execute_decision(decision)
|
|
544
|
+
puts " Result: #{JSON.pretty_generate(result)}"
|
|
545
|
+
end
|
|
546
|
+
rescue Ollama::Error => e
|
|
547
|
+
puts "ā Error: #{e.message}"
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
puts
|
|
551
|
+
puts "ā" * 60
|
|
552
|
+
puts "Example 2: All Data APIs Demonstration"
|
|
553
|
+
puts "ā" * 60
|
|
554
|
+
puts "Demonstrating all available DhanHQ Data APIs:"
|
|
555
|
+
puts
|
|
556
|
+
|
|
557
|
+
test_symbol = "RELIANCE" # RELIANCE symbol for Instrument.find
|
|
558
|
+
test_exchange = "NSE_EQ"
|
|
559
|
+
|
|
560
|
+
# 1. Market Quote (uses Instrument convenience method)
|
|
561
|
+
puts "1ļøā£ Market Quote API"
|
|
562
|
+
begin
|
|
563
|
+
result = DhanHQDataTools.get_market_quote(symbol: test_symbol, exchange_segment: test_exchange)
|
|
564
|
+
if result[:result]
|
|
565
|
+
puts " ā
Market Quote retrieved"
|
|
566
|
+
puts " š Quote data: #{result[:result][:quote].inspect[0..150]}"
|
|
567
|
+
else
|
|
568
|
+
puts " ā ļø #{result[:error]}"
|
|
569
|
+
end
|
|
570
|
+
rescue StandardError => e
|
|
571
|
+
puts " ā Error: #{e.message}"
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
puts
|
|
575
|
+
sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
|
|
576
|
+
|
|
577
|
+
# 2. Live Market Feed (LTP) (uses Instrument convenience method)
|
|
578
|
+
puts "2ļøā£ Live Market Feed API (LTP)"
|
|
579
|
+
begin
|
|
580
|
+
result = DhanHQDataTools.get_live_ltp(symbol: test_symbol, exchange_segment: test_exchange)
|
|
581
|
+
if result[:result]
|
|
582
|
+
puts " ā
LTP retrieved"
|
|
583
|
+
puts " š LTP: #{result[:result][:ltp].inspect}"
|
|
584
|
+
else
|
|
585
|
+
puts " ā ļø #{result[:error]}"
|
|
586
|
+
end
|
|
587
|
+
rescue StandardError => e
|
|
588
|
+
puts " ā Error: #{e.message}"
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
puts
|
|
592
|
+
sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
|
|
593
|
+
|
|
594
|
+
# 3. Full Market Depth (uses Instrument convenience method)
|
|
595
|
+
puts "3ļøā£ Full Market Depth API"
|
|
596
|
+
begin
|
|
597
|
+
result = DhanHQDataTools.get_market_depth(symbol: test_symbol, exchange_segment: test_exchange)
|
|
598
|
+
if result[:result]
|
|
599
|
+
puts " ā
Market Depth retrieved"
|
|
600
|
+
puts " š Buy depth: #{result[:result][:buy_depth]&.length || 0} levels"
|
|
601
|
+
puts " š Sell depth: #{result[:result][:sell_depth]&.length || 0} levels"
|
|
602
|
+
puts " š LTP: #{result[:result][:ltp]}"
|
|
603
|
+
puts " š Volume: #{result[:result][:volume]}"
|
|
604
|
+
else
|
|
605
|
+
puts " ā ļø #{result[:error]}"
|
|
606
|
+
end
|
|
607
|
+
rescue StandardError => e
|
|
608
|
+
puts " ā Error: #{e.message}"
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
puts
|
|
612
|
+
sleep(1.2) # Rate limit: 1 request per second for MarketFeed APIs
|
|
613
|
+
|
|
614
|
+
# 4. Historical Data (uses symbol for Instrument.find)
|
|
615
|
+
puts "4ļøā£ Historical Data API"
|
|
616
|
+
begin
|
|
617
|
+
result = DhanHQDataTools.get_historical_data(
|
|
618
|
+
symbol: test_symbol,
|
|
619
|
+
exchange_segment: test_exchange,
|
|
620
|
+
from_date: "2024-01-01",
|
|
621
|
+
to_date: "2024-01-31"
|
|
622
|
+
)
|
|
623
|
+
if result[:result]
|
|
624
|
+
puts " ā
Historical data retrieved"
|
|
625
|
+
puts " š Type: #{result[:type]}"
|
|
626
|
+
puts " š Records: #{result[:result][:count]}"
|
|
627
|
+
else
|
|
628
|
+
puts " ā ļø #{result[:error]}"
|
|
629
|
+
end
|
|
630
|
+
rescue StandardError => e
|
|
631
|
+
puts " ā Error: #{e.message}"
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
puts
|
|
635
|
+
sleep(0.5) # Small delay for Instrument APIs
|
|
636
|
+
|
|
637
|
+
# 5. Expired Options Data (uses symbol for Instrument.find)
|
|
638
|
+
puts "5ļøā£ Expired Options Data API"
|
|
639
|
+
begin
|
|
640
|
+
# Use NSE_FNO for options
|
|
641
|
+
result = DhanHQDataTools.get_expired_options_data(
|
|
642
|
+
symbol: test_symbol,
|
|
643
|
+
exchange_segment: "NSE_FNO",
|
|
644
|
+
expiry_date: "2024-01-31"
|
|
645
|
+
)
|
|
646
|
+
if result[:result]
|
|
647
|
+
puts " ā
Expired options data retrieved"
|
|
648
|
+
puts " š Expiry: #{result[:result][:expiry_date]}"
|
|
649
|
+
puts " š Data: #{result[:result][:data].inspect[0..150]}"
|
|
650
|
+
else
|
|
651
|
+
puts " ā ļø #{result[:error]}"
|
|
652
|
+
end
|
|
653
|
+
rescue StandardError => e
|
|
654
|
+
puts " ā Error: #{e.message}"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
puts
|
|
658
|
+
sleep(0.5) # Small delay for Instrument APIs
|
|
659
|
+
|
|
660
|
+
# 6. Option Chain (uses symbol for Instrument.find)
|
|
661
|
+
puts "6ļøā£ Option Chain API"
|
|
662
|
+
begin
|
|
663
|
+
result = DhanHQDataTools.get_option_chain(
|
|
664
|
+
symbol: test_symbol,
|
|
665
|
+
exchange_segment: "NSE_FNO"
|
|
666
|
+
)
|
|
667
|
+
if result[:result]
|
|
668
|
+
puts " ā
Option chain data retrieved"
|
|
669
|
+
if result[:result][:expiries]
|
|
670
|
+
puts " š Available expiries: #{result[:result][:count]}"
|
|
671
|
+
expiries = result[:result][:expiries]
|
|
672
|
+
puts " š First few expiries: #{expiries.first(3).inspect}" if expiries.is_a?(Array)
|
|
673
|
+
elsif result[:result][:chain]
|
|
674
|
+
puts " š Option chain: #{result[:result][:chain].inspect[0..150]}"
|
|
675
|
+
end
|
|
676
|
+
else
|
|
677
|
+
puts " ā ļø #{result[:error]}"
|
|
678
|
+
end
|
|
679
|
+
rescue StandardError => e
|
|
680
|
+
puts " ā Error: #{e.message}"
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
puts
|
|
684
|
+
puts "=" * 60
|
|
685
|
+
puts "TRADING AGENT: Order Parameter Building"
|
|
686
|
+
puts "=" * 60
|
|
687
|
+
puts
|
|
688
|
+
|
|
689
|
+
# ============================================================
|
|
690
|
+
# TRADING AGENT EXAMPLES
|
|
691
|
+
# ============================================================
|
|
692
|
+
config = Ollama::Config.new
|
|
693
|
+
config.timeout = 60
|
|
694
|
+
trading_ollama_client = Ollama::Client.new(config: config)
|
|
695
|
+
trading_agent = TradingAgent.new(ollama_client: trading_ollama_client)
|
|
696
|
+
|
|
697
|
+
# Example 1: Simple buy order
|
|
698
|
+
puts "Example 1: Simple Buy Order"
|
|
699
|
+
puts "ā" * 60
|
|
700
|
+
|
|
701
|
+
market_context = <<~CONTEXT
|
|
702
|
+
RELIANCE is showing strong momentum.
|
|
703
|
+
Current LTP: 2,850
|
|
704
|
+
Entry price: 2,850
|
|
705
|
+
Quantity: 100 shares
|
|
706
|
+
Use regular order. security_id="1333", exchange_segment="NSE_EQ"
|
|
707
|
+
CONTEXT
|
|
708
|
+
|
|
709
|
+
puts "Market Context:"
|
|
710
|
+
puts market_context
|
|
711
|
+
puts
|
|
712
|
+
|
|
713
|
+
begin
|
|
714
|
+
puts "š¤ Analyzing with Ollama..."
|
|
715
|
+
decision = trading_agent.analyze_and_decide(market_context: market_context)
|
|
716
|
+
|
|
717
|
+
puts "\nš Decision:"
|
|
718
|
+
if decision.is_a?(Hash)
|
|
719
|
+
puts " Action: #{decision['action'] || 'N/A'}"
|
|
720
|
+
puts " Reasoning: #{decision['reasoning'] || 'N/A'}"
|
|
721
|
+
puts " Confidence: #{(decision['confidence'] * 100).round}%" if decision["confidence"]
|
|
722
|
+
puts " Parameters: #{JSON.pretty_generate(decision['parameters'] || {})}"
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
if decision["action"] != "no_action"
|
|
726
|
+
puts "\nā” Building order parameters (order not placed)..."
|
|
727
|
+
result = trading_agent.execute_decision(decision)
|
|
728
|
+
puts " Result: #{JSON.pretty_generate(result)}"
|
|
729
|
+
if result.is_a?(Hash) && result[:order_params]
|
|
730
|
+
puts "\n š Order Parameters Ready:"
|
|
731
|
+
puts " #{JSON.pretty_generate(result[:order_params])}"
|
|
732
|
+
puts " š” To place order: DhanHQ::Models::Order.new(result[:order_params]).save"
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
rescue Ollama::TimeoutError => e
|
|
736
|
+
puts "ā±ļø Timeout: #{e.message}"
|
|
737
|
+
rescue Ollama::Error => e
|
|
738
|
+
puts "ā Error: #{e.message}"
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
puts
|
|
742
|
+
puts "=" * 60
|
|
743
|
+
puts "DhanHQ Agent Summary:"
|
|
744
|
+
puts " ā
Ollama: Reasoning & Decision Making"
|
|
745
|
+
puts " ā
DhanHQ: Data Retrieval & Order Building"
|
|
746
|
+
puts " ā
Data APIs: Market Quote, Live Market Feed, Full Market Depth, " \
|
|
747
|
+
"Historical Data, Expired Options Data, Option Chain"
|
|
748
|
+
puts " ā
Trading Tools: Order parameters, Super order parameters, Cancel parameters"
|
|
749
|
+
puts " ā
Instrument Convenience Methods: ltp, ohlc, quote, daily, intraday, expiry_list, option_chain"
|
|
750
|
+
puts "=" * 60
|
|
751
|
+
end
|
|
752
|
+
|