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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +336 -91
- data/RELEASE_NOTES_v0.2.6.md +41 -0
- data/docs/AREAS_FOR_CONSIDERATION.md +325 -0
- data/docs/EXAMPLE_REORGANIZATION.md +412 -0
- data/docs/FEATURES_ADDED.md +12 -1
- data/docs/GETTING_STARTED.md +361 -0
- data/docs/INTEGRATION_TESTING.md +170 -0
- data/docs/NEXT_STEPS_SUMMARY.md +114 -0
- data/docs/PERSONAS.md +383 -0
- data/docs/QUICK_START.md +195 -0
- data/docs/TESTING.md +392 -170
- data/docs/TEST_CHECKLIST.md +450 -0
- data/examples/README.md +62 -63
- data/examples/basic_chat.rb +33 -0
- data/examples/basic_generate.rb +29 -0
- data/examples/mcp_executor.rb +39 -0
- data/examples/mcp_http_executor.rb +45 -0
- data/examples/tool_calling_parsing.rb +59 -0
- data/examples/tool_dto_example.rb +0 -0
- data/exe/ollama-client +128 -1
- data/lib/ollama/agent/planner.rb +7 -2
- data/lib/ollama/chat_session.rb +101 -0
- data/lib/ollama/client.rb +41 -35
- data/lib/ollama/config.rb +9 -4
- data/lib/ollama/document_loader.rb +1 -1
- data/lib/ollama/embeddings.rb +61 -28
- data/lib/ollama/errors.rb +1 -0
- data/lib/ollama/mcp/http_client.rb +149 -0
- data/lib/ollama/mcp/stdio_client.rb +146 -0
- data/lib/ollama/mcp/tools_bridge.rb +72 -0
- data/lib/ollama/mcp.rb +31 -0
- data/lib/ollama/options.rb +3 -1
- data/lib/ollama/personas.rb +287 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +17 -5
- metadata +22 -48
- data/examples/advanced_complex_schemas.rb +0 -366
- data/examples/advanced_edge_cases.rb +0 -241
- data/examples/advanced_error_handling.rb +0 -200
- data/examples/advanced_multi_step_agent.rb +0 -341
- data/examples/advanced_performance_testing.rb +0 -186
- data/examples/chat_console.rb +0 -143
- data/examples/complete_workflow.rb +0 -245
- data/examples/dhan_console.rb +0 -843
- data/examples/dhanhq/README.md +0 -236
- data/examples/dhanhq/agents/base_agent.rb +0 -74
- data/examples/dhanhq/agents/data_agent.rb +0 -66
- data/examples/dhanhq/agents/orchestrator_agent.rb +0 -120
- data/examples/dhanhq/agents/technical_analysis_agent.rb +0 -252
- data/examples/dhanhq/agents/trading_agent.rb +0 -81
- data/examples/dhanhq/analysis/market_structure.rb +0 -138
- data/examples/dhanhq/analysis/pattern_recognizer.rb +0 -192
- data/examples/dhanhq/analysis/trend_analyzer.rb +0 -88
- data/examples/dhanhq/builders/market_context_builder.rb +0 -67
- data/examples/dhanhq/dhanhq_agent.rb +0 -829
- data/examples/dhanhq/indicators/technical_indicators.rb +0 -158
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +0 -492
- data/examples/dhanhq/scanners/swing_scanner.rb +0 -247
- data/examples/dhanhq/schemas/agent_schemas.rb +0 -61
- data/examples/dhanhq/services/base_service.rb +0 -46
- data/examples/dhanhq/services/data_service.rb +0 -118
- data/examples/dhanhq/services/trading_service.rb +0 -59
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +0 -411
- data/examples/dhanhq/technical_analysis_runner.rb +0 -420
- data/examples/dhanhq/test_tool_calling.rb +0 -538
- data/examples/dhanhq/test_tool_calling_verbose.rb +0 -251
- data/examples/dhanhq/utils/instrument_helper.rb +0 -32
- data/examples/dhanhq/utils/parameter_cleaner.rb +0 -28
- data/examples/dhanhq/utils/parameter_normalizer.rb +0 -45
- data/examples/dhanhq/utils/rate_limiter.rb +0 -23
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +0 -72
- data/examples/dhanhq_agent.rb +0 -964
- data/examples/dhanhq_tools.rb +0 -1663
- data/examples/multi_step_agent_with_external_data.rb +0 -368
- data/examples/structured_outputs_chat.rb +0 -72
- data/examples/structured_tools.rb +0 -89
- data/examples/test_dhanhq_tool_calling.rb +0 -375
- data/examples/test_tool_calling.rb +0 -160
- data/examples/tool_calling_direct.rb +0 -124
- data/examples/tool_calling_pattern.rb +0 -269
- data/exe/dhan_console +0 -4
data/examples/dhanhq_agent.rb
DELETED
|
@@ -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
|
-
|