ollama-client 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +220 -12
- data/docs/CLOUD.md +29 -0
- data/docs/CONSOLE_IMPROVEMENTS.md +256 -0
- data/docs/FEATURES_ADDED.md +145 -0
- data/docs/HANDLERS_ANALYSIS.md +190 -0
- data/docs/README.md +37 -0
- data/docs/SCHEMA_FIXES.md +147 -0
- data/docs/TEST_UPDATES.md +107 -0
- data/examples/README.md +92 -0
- data/examples/advanced_complex_schemas.rb +6 -3
- data/examples/advanced_multi_step_agent.rb +13 -7
- data/examples/chat_console.rb +143 -0
- data/examples/complete_workflow.rb +14 -4
- data/examples/dhan_console.rb +843 -0
- data/examples/dhanhq/agents/base_agent.rb +0 -2
- data/examples/dhanhq/agents/orchestrator_agent.rb +1 -2
- data/examples/dhanhq/agents/technical_analysis_agent.rb +67 -49
- data/examples/dhanhq/analysis/market_structure.rb +44 -28
- data/examples/dhanhq/analysis/pattern_recognizer.rb +64 -47
- data/examples/dhanhq/analysis/trend_analyzer.rb +6 -8
- data/examples/dhanhq/dhanhq_agent.rb +296 -99
- data/examples/dhanhq/indicators/technical_indicators.rb +3 -5
- data/examples/dhanhq/scanners/intraday_options_scanner.rb +360 -255
- data/examples/dhanhq/scanners/swing_scanner.rb +118 -84
- data/examples/dhanhq/schemas/agent_schemas.rb +2 -2
- data/examples/dhanhq/services/data_service.rb +5 -7
- data/examples/dhanhq/services/trading_service.rb +0 -3
- data/examples/dhanhq/technical_analysis_agentic_runner.rb +217 -84
- data/examples/dhanhq/technical_analysis_runner.rb +216 -162
- data/examples/dhanhq/test_tool_calling.rb +538 -0
- data/examples/dhanhq/test_tool_calling_verbose.rb +251 -0
- data/examples/dhanhq/utils/trading_parameter_normalizer.rb +12 -17
- data/examples/dhanhq_agent.rb +159 -116
- data/examples/dhanhq_tools.rb +1158 -251
- data/examples/multi_step_agent_with_external_data.rb +368 -0
- data/examples/structured_tools.rb +89 -0
- data/examples/test_dhanhq_tool_calling.rb +375 -0
- data/examples/test_tool_calling.rb +160 -0
- data/examples/tool_calling_direct.rb +124 -0
- data/examples/tool_dto_example.rb +94 -0
- data/exe/dhan_console +4 -0
- data/exe/ollama-client +1 -1
- data/lib/ollama/agent/executor.rb +116 -15
- data/lib/ollama/client.rb +118 -55
- data/lib/ollama/config.rb +36 -0
- data/lib/ollama/dto.rb +187 -0
- data/lib/ollama/embeddings.rb +77 -0
- data/lib/ollama/options.rb +104 -0
- data/lib/ollama/response.rb +121 -0
- data/lib/ollama/tool/function/parameters/property.rb +72 -0
- data/lib/ollama/tool/function/parameters.rb +101 -0
- data/lib/ollama/tool/function.rb +78 -0
- data/lib/ollama/tool.rb +60 -0
- data/lib/ollama/version.rb +1 -1
- data/lib/ollama_client.rb +3 -0
- metadata +31 -3
- /data/{PRODUCTION_FIXES.md → docs/PRODUCTION_FIXES.md} +0 -0
- /data/{TESTING.md → docs/TESTING.md} +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# DhanHQ Tool Calling Test - Verbose Version
|
|
5
|
+
# Shows the complete tool calling flow: LLM decides → Executor executes
|
|
6
|
+
|
|
7
|
+
require_relative "../../lib/ollama_client"
|
|
8
|
+
require_relative "../dhanhq_tools"
|
|
9
|
+
|
|
10
|
+
puts "\n=== DHANHQ TOOL CALLING TEST (VERBOSE) ===\n"
|
|
11
|
+
puts "This demonstrates REAL tool calling:\n"
|
|
12
|
+
puts " 1. LLM receives user query + tool definitions"
|
|
13
|
+
puts " 2. LLM DECIDES which tools to call (not your code!)"
|
|
14
|
+
puts " 3. LLM returns tool_calls in response"
|
|
15
|
+
puts " 4. Executor detects tool_calls and executes callables"
|
|
16
|
+
puts " 5. Tool results fed back to LLM"
|
|
17
|
+
puts " 6. LLM generates final answer\n"
|
|
18
|
+
|
|
19
|
+
# Configure DhanHQ
|
|
20
|
+
begin
|
|
21
|
+
DhanHQ.configure_with_env
|
|
22
|
+
puts "✅ DhanHQ configured\n"
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
puts "⚠️ DhanHQ configuration error: #{e.message}\n"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Create client
|
|
28
|
+
config = Ollama::Config.new
|
|
29
|
+
config.model = ENV.fetch("OLLAMA_MODEL", "llama3.1:8b")
|
|
30
|
+
config.temperature = 0.2
|
|
31
|
+
config.timeout = 60
|
|
32
|
+
client = Ollama::Client.new(config: config)
|
|
33
|
+
|
|
34
|
+
# Define tools
|
|
35
|
+
market_quote_tool = Ollama::Tool.new(
|
|
36
|
+
type: "function",
|
|
37
|
+
function: Ollama::Tool::Function.new(
|
|
38
|
+
name: "get_market_quote",
|
|
39
|
+
description: "Get market quote for a symbol",
|
|
40
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Stock symbol"
|
|
46
|
+
),
|
|
47
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "Exchange segment",
|
|
50
|
+
enum: %w[NSE_EQ BSE_EQ IDX_I]
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
required: %w[symbol exchange_segment]
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
live_ltp_tool = Ollama::Tool.new(
|
|
59
|
+
type: "function",
|
|
60
|
+
function: Ollama::Tool::Function.new(
|
|
61
|
+
name: "get_live_ltp",
|
|
62
|
+
description: "Get live last traded price",
|
|
63
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Stock symbol"
|
|
69
|
+
),
|
|
70
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Exchange segment",
|
|
73
|
+
enum: %w[NSE_EQ BSE_EQ IDX_I]
|
|
74
|
+
)
|
|
75
|
+
},
|
|
76
|
+
required: %w[symbol exchange_segment]
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
option_chain_tool = Ollama::Tool.new(
|
|
82
|
+
type: "function",
|
|
83
|
+
function: Ollama::Tool::Function.new(
|
|
84
|
+
name: "get_option_chain",
|
|
85
|
+
description: "Get option chain for an index (NIFTY, SENSEX, BANKNIFTY). " \
|
|
86
|
+
"Returns available expiries and option chain data with strikes, Greeks, OI, and IV.",
|
|
87
|
+
parameters: Ollama::Tool::Function::Parameters.new(
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
symbol: Ollama::Tool::Function::Parameters::Property.new(
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Index symbol (NIFTY, SENSEX, or BANKNIFTY)",
|
|
93
|
+
enum: %w[NIFTY SENSEX BANKNIFTY]
|
|
94
|
+
),
|
|
95
|
+
exchange_segment: Ollama::Tool::Function::Parameters::Property.new(
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Exchange segment (must be IDX_I for indices)",
|
|
98
|
+
enum: %w[IDX_I]
|
|
99
|
+
),
|
|
100
|
+
expiry: Ollama::Tool::Function::Parameters::Property.new(
|
|
101
|
+
type: "string",
|
|
102
|
+
description: "Optional expiry date (YYYY-MM-DD format). If not provided, returns available expiries list."
|
|
103
|
+
)
|
|
104
|
+
},
|
|
105
|
+
required: %w[symbol exchange_segment]
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Define callables (these are just implementations - LLM decides when to call them)
|
|
111
|
+
tools = {
|
|
112
|
+
"get_option_chain" => {
|
|
113
|
+
tool: option_chain_tool,
|
|
114
|
+
callable: lambda do |symbol:, exchange_segment:, expiry: nil|
|
|
115
|
+
# Normalize empty string to nil (LLM might pass "" when expiry is optional)
|
|
116
|
+
expiry = nil if expiry.is_a?(String) && expiry.empty?
|
|
117
|
+
puts "\n [TOOL EXECUTION] get_option_chain called by Executor"
|
|
118
|
+
puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}, expiry=#{expiry || 'nil'}"
|
|
119
|
+
puts " Note: This is called AFTER LLM decided to use this tool!"
|
|
120
|
+
result = DhanHQDataTools.get_option_chain(
|
|
121
|
+
symbol: symbol.to_s,
|
|
122
|
+
exchange_segment: exchange_segment.to_s,
|
|
123
|
+
expiry: expiry
|
|
124
|
+
)
|
|
125
|
+
if result[:error]
|
|
126
|
+
{ error: result[:error] }
|
|
127
|
+
elsif result[:result] && result[:result][:expiries]
|
|
128
|
+
# Return expiry list
|
|
129
|
+
{
|
|
130
|
+
symbol: symbol,
|
|
131
|
+
expiries_available: result[:result][:expiries],
|
|
132
|
+
count: result[:result][:count]
|
|
133
|
+
}
|
|
134
|
+
elsif result[:result] && result[:result][:chain]
|
|
135
|
+
# Return option chain data
|
|
136
|
+
chain = result[:result][:chain]
|
|
137
|
+
strikes = chain.is_a?(Hash) ? chain.keys.sort_by(&:to_f) : []
|
|
138
|
+
{
|
|
139
|
+
symbol: symbol,
|
|
140
|
+
expiry: result[:result][:expiry],
|
|
141
|
+
underlying_price: result[:result][:underlying_last_price],
|
|
142
|
+
strikes_count: strikes.length,
|
|
143
|
+
sample_strikes: strikes.first(5)
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
{ error: "Unexpected response format" }
|
|
147
|
+
end
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
{ error: e.message }
|
|
150
|
+
end
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
"get_market_quote" => {
|
|
154
|
+
tool: market_quote_tool,
|
|
155
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
156
|
+
puts "\n [TOOL EXECUTION] get_market_quote called by Executor"
|
|
157
|
+
puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
|
|
158
|
+
puts " Note: This is called AFTER LLM decided to use this tool!"
|
|
159
|
+
result = DhanHQDataTools.get_market_quote(
|
|
160
|
+
symbol: symbol.to_s,
|
|
161
|
+
exchange_segment: exchange_segment.to_s
|
|
162
|
+
)
|
|
163
|
+
if result[:error]
|
|
164
|
+
{ error: result[:error] }
|
|
165
|
+
else
|
|
166
|
+
quote = result[:result][:quote]
|
|
167
|
+
{
|
|
168
|
+
symbol: symbol,
|
|
169
|
+
last_price: quote[:last_price],
|
|
170
|
+
volume: quote[:volume],
|
|
171
|
+
ohlc: quote[:ohlc]
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
"get_live_ltp" => {
|
|
178
|
+
tool: live_ltp_tool,
|
|
179
|
+
callable: lambda do |symbol:, exchange_segment:|
|
|
180
|
+
puts "\n [TOOL EXECUTION] get_live_ltp called by Executor"
|
|
181
|
+
puts " Args: symbol=#{symbol}, exchange_segment=#{exchange_segment}"
|
|
182
|
+
puts " Note: This is called AFTER LLM decided to use this tool!"
|
|
183
|
+
sleep(1.2) # Rate limiting
|
|
184
|
+
result = DhanHQDataTools.get_live_ltp(
|
|
185
|
+
symbol: symbol.to_s,
|
|
186
|
+
exchange_segment: exchange_segment.to_s
|
|
187
|
+
)
|
|
188
|
+
if result[:error]
|
|
189
|
+
{ error: result[:error] }
|
|
190
|
+
else
|
|
191
|
+
{
|
|
192
|
+
symbol: symbol,
|
|
193
|
+
ltp: result[:result][:ltp]
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
puts "--- Step 1: Show what tools are available to LLM ---"
|
|
201
|
+
puts "Tools defined:"
|
|
202
|
+
puts " - get_market_quote: Get market quote"
|
|
203
|
+
puts " - get_live_ltp: Get live price"
|
|
204
|
+
puts " - get_option_chain: Get option chain for indices (NIFTY, SENSEX, BANKNIFTY)"
|
|
205
|
+
puts "\nThese tool DEFINITIONS are sent to LLM (not executed yet)\n"
|
|
206
|
+
|
|
207
|
+
puts "--- Step 2: LLM receives query and DECIDES which tools to call ---"
|
|
208
|
+
puts "User query: 'Get RELIANCE quote, NIFTY price, and SENSEX option chain'\n"
|
|
209
|
+
puts "LLM will analyze this and decide to call:"
|
|
210
|
+
puts " 1. get_market_quote(RELIANCE, NSE_EQ)"
|
|
211
|
+
puts " 2. get_live_ltp(NIFTY, IDX_I)"
|
|
212
|
+
puts " 3. get_option_chain(SENSEX, IDX_I)"
|
|
213
|
+
puts "\nThis decision is made by the LLM, not by your code!\n"
|
|
214
|
+
|
|
215
|
+
puts "--- Step 3: Executor sends request to LLM with tool definitions ---"
|
|
216
|
+
puts "Sending to LLM via chat_raw() with tools parameter...\n"
|
|
217
|
+
|
|
218
|
+
# Create executor
|
|
219
|
+
executor = Ollama::Agent::Executor.new(
|
|
220
|
+
client,
|
|
221
|
+
tools: tools,
|
|
222
|
+
max_steps: 10
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
result = executor.run(
|
|
227
|
+
system: "You are a market data assistant. Use the available tools to get market data. " \
|
|
228
|
+
"For option chains, you can get SENSEX options using get_option_chain with " \
|
|
229
|
+
"symbol='SENSEX' and exchange_segment='IDX_I'.",
|
|
230
|
+
user: "Get market quote for RELIANCE stock, check NIFTY's current price, and get SENSEX option chain"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
puts "\n--- Step 4: Final result from LLM (after tool execution) ---"
|
|
234
|
+
puts "=" * 60
|
|
235
|
+
puts result
|
|
236
|
+
puts "=" * 60
|
|
237
|
+
rescue Ollama::Error => e
|
|
238
|
+
puts "\n❌ Error: #{e.message}"
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
puts "\n❌ Unexpected error: #{e.message}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
puts "\n--- Summary ---"
|
|
244
|
+
puts "✅ This IS real tool calling:"
|
|
245
|
+
puts " - LLM decides which tools to call (not your code)"
|
|
246
|
+
puts " - LLM returns tool_calls in response"
|
|
247
|
+
puts " - Executor detects tool_calls and executes callables"
|
|
248
|
+
puts " - Tool results fed back to LLM automatically"
|
|
249
|
+
puts " - LLM generates final answer based on tool results"
|
|
250
|
+
puts "\nThe callables are just implementations - the LLM decides WHEN to call them!"
|
|
251
|
+
puts "\n=== DONE ===\n"
|
|
@@ -26,9 +26,7 @@ module DhanHQ
|
|
|
26
26
|
return nil if value.nil?
|
|
27
27
|
|
|
28
28
|
# If already a number, return as-is (but validate it's reasonable)
|
|
29
|
-
if value.is_a?(Numeric)
|
|
30
|
-
return value
|
|
31
|
-
end
|
|
29
|
+
return value if value.is_a?(Numeric)
|
|
32
30
|
|
|
33
31
|
# If string, clean and convert
|
|
34
32
|
return nil unless value.is_a?(String)
|
|
@@ -38,28 +36,25 @@ module DhanHQ
|
|
|
38
36
|
return nil if cleaned.empty?
|
|
39
37
|
|
|
40
38
|
# Try to convert to float (for prices) or integer (for quantity)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
result
|
|
39
|
+
if cleaned.include?(".")
|
|
40
|
+
cleaned.to_f
|
|
41
|
+
else
|
|
42
|
+
cleaned.to_i
|
|
43
|
+
end
|
|
48
44
|
rescue StandardError
|
|
49
45
|
nil
|
|
50
46
|
end
|
|
51
47
|
|
|
52
48
|
# Validates if a price value seems reasonable (not obviously wrong)
|
|
53
|
-
def self.
|
|
49
|
+
def self.valid_price?(price, context_hint = nil)
|
|
54
50
|
return false if price.nil?
|
|
55
51
|
|
|
56
52
|
# If price is suspiciously low (< 10), it might be wrong
|
|
57
53
|
# But we can't be too strict since some stocks might legitimately be < 10
|
|
58
|
-
if
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
54
|
+
# Check if context hint suggests a higher price
|
|
55
|
+
if price.is_a?(Numeric) && price.positive? && price < 10 &&
|
|
56
|
+
context_hint.is_a?(Numeric) && context_hint > price * 100
|
|
57
|
+
return false # Likely wrong
|
|
63
58
|
end
|
|
64
59
|
|
|
65
60
|
true
|
|
@@ -71,7 +66,7 @@ module DhanHQ
|
|
|
71
66
|
value.to_s
|
|
72
67
|
end
|
|
73
68
|
|
|
74
|
-
private_class_method :normalize_numeric, :normalize_security_id
|
|
69
|
+
private_class_method :normalize_numeric, :normalize_security_id, :valid_price?
|
|
75
70
|
end
|
|
76
71
|
end
|
|
77
72
|
end
|
data/examples/dhanhq_agent.rb
CHANGED
|
@@ -76,7 +76,7 @@ class DataAgent
|
|
|
76
76
|
"type" => "number",
|
|
77
77
|
"minimum" => 0,
|
|
78
78
|
"maximum" => 1,
|
|
79
|
-
"description" => "Confidence in this decision"
|
|
79
|
+
"description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
|
|
80
80
|
},
|
|
81
81
|
"parameters" => {
|
|
82
82
|
"type" => "object",
|
|
@@ -304,7 +304,7 @@ class TradingAgent
|
|
|
304
304
|
"type" => "number",
|
|
305
305
|
"minimum" => 0,
|
|
306
306
|
"maximum" => 1,
|
|
307
|
-
"description" => "Confidence in this decision"
|
|
307
|
+
"description" => "Confidence in this decision (0.0 to 1.0, where 1.0 is 100% confident)"
|
|
308
308
|
},
|
|
309
309
|
"parameters" => {
|
|
310
310
|
"type" => "object",
|
|
@@ -443,6 +443,139 @@ class TradingAgent
|
|
|
443
443
|
end
|
|
444
444
|
end
|
|
445
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
|
+
|
|
446
579
|
# Main execution
|
|
447
580
|
if __FILE__ == $PROGRAM_NAME
|
|
448
581
|
# Configure DhanHQ (must be done before using DhanHQ models)
|
|
@@ -529,16 +662,26 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
529
662
|
puts " ⚠️ RELIANCE data error: #{e.message}"
|
|
530
663
|
end
|
|
531
664
|
|
|
532
|
-
# NOTE: Positions and holdings are not part of the 6 Data APIs
|
|
665
|
+
# NOTE: Positions and holdings are not part of the 6 Data APIs, but available via DhanHQ gem
|
|
533
666
|
begin
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
|
542
685
|
end
|
|
543
686
|
rescue StandardError => e
|
|
544
687
|
puts " ⚠️ Positions error: #{e.message}"
|
|
@@ -661,7 +804,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
661
804
|
puts " ✅ Historical data retrieved"
|
|
662
805
|
puts " 📊 Type: #{result[:type]}"
|
|
663
806
|
puts " 📊 Records: #{result[:result][:count]}"
|
|
664
|
-
if result[:result][:count]
|
|
807
|
+
if result[:result][:count].zero?
|
|
665
808
|
puts " ⚠️ No data found for date range #{from_date} to #{to_date}"
|
|
666
809
|
puts " (This may be normal if market was closed or data unavailable)"
|
|
667
810
|
end
|
|
@@ -698,27 +841,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
698
841
|
# Show concise summary of expired options data instead of full data (can be very large)
|
|
699
842
|
if result[:result][:summary_stats]
|
|
700
843
|
stats = result[:result][:summary_stats]
|
|
701
|
-
|
|
702
|
-
concise_summary = {
|
|
703
|
-
data_points: stats[:data_points] || 0,
|
|
704
|
-
avg_volume: stats[:avg_volume]&.round(2),
|
|
705
|
-
avg_open_interest: stats[:avg_open_interest]&.round(2),
|
|
706
|
-
avg_implied_volatility: stats[:avg_implied_volatility]&.round(4),
|
|
707
|
-
price_range_stats: if stats[:price_ranges]&.is_a?(Array) && !stats[:price_ranges].empty?
|
|
708
|
-
{
|
|
709
|
-
min: stats[:price_ranges].min.round(2),
|
|
710
|
-
max: stats[:price_ranges].max.round(2),
|
|
711
|
-
avg: (stats[:price_ranges].sum / stats[:price_ranges].length).round(2),
|
|
712
|
-
count: stats[:price_ranges].length
|
|
713
|
-
}
|
|
714
|
-
else
|
|
715
|
-
nil
|
|
716
|
-
end,
|
|
717
|
-
has_ohlc: stats[:has_ohlc],
|
|
718
|
-
has_volume: stats[:has_volume],
|
|
719
|
-
has_open_interest: stats[:has_open_interest],
|
|
720
|
-
has_implied_volatility: stats[:has_implied_volatility]
|
|
721
|
-
}
|
|
844
|
+
concise_summary = build_expired_options_summary(stats)
|
|
722
845
|
puts " 📊 Data summary: #{JSON.pretty_generate(concise_summary)}"
|
|
723
846
|
else
|
|
724
847
|
puts " 📊 Data available but summary stats not found"
|
|
@@ -739,8 +862,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
739
862
|
begin
|
|
740
863
|
# NOTE: Options symbols may need different format
|
|
741
864
|
# Try with NIFTY which typically has options
|
|
742
|
-
# First, get the list of available expiries
|
|
743
|
-
expiry_list_result = DhanHQDataTools.
|
|
865
|
+
# First, get the list of available expiries using get_expiry_list
|
|
866
|
+
expiry_list_result = DhanHQDataTools.get_expiry_list(
|
|
744
867
|
symbol: "NIFTY", # NIFTY typically has options, RELIANCE might not
|
|
745
868
|
exchange_segment: "IDX_I"
|
|
746
869
|
)
|
|
@@ -759,87 +882,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
759
882
|
exchange_segment: "IDX_I", # Use IDX_I for index options underlying
|
|
760
883
|
expiry: next_expiry
|
|
761
884
|
)
|
|
762
|
-
|
|
763
|
-
chain = chain_result[:result][:chain]
|
|
764
|
-
underlying_price = chain_result[:result][:underlying_last_price]
|
|
765
|
-
|
|
766
|
-
# Option chain structure: chain[:oc] is a hash with strike prices as string keys
|
|
767
|
-
# Each strike contains "ce" (call) and "pe" (put) data with Greeks, IV, OI, etc.
|
|
768
|
-
chain_summary = if chain.is_a?(Hash)
|
|
769
|
-
# Strike prices are string keys (e.g., "25000.000000")
|
|
770
|
-
strike_prices = chain.keys.sort_by { |k| k.to_f }
|
|
771
|
-
first_strike_data = chain[strike_prices.first] unless strike_prices.empty?
|
|
772
|
-
|
|
773
|
-
# Find ATM or nearest strike to underlying price for sample Greeks
|
|
774
|
-
atm_strike = if underlying_price && !strike_prices.empty?
|
|
775
|
-
strike_prices.min_by { |s| (s.to_f - underlying_price).abs }
|
|
776
|
-
else
|
|
777
|
-
strike_prices.first
|
|
778
|
-
end
|
|
779
|
-
atm_data = chain[atm_strike] if atm_strike
|
|
780
|
-
|
|
781
|
-
# Extract Greeks from ATM strike if available
|
|
782
|
-
sample_greeks = {}
|
|
783
|
-
if atm_data.is_a?(Hash)
|
|
784
|
-
ce_data = atm_data["ce"] || atm_data[:ce]
|
|
785
|
-
pe_data = atm_data["pe"] || atm_data[:pe]
|
|
786
|
-
|
|
787
|
-
if ce_data.is_a?(Hash) && (ce_data.key?("greeks") || ce_data.key?(:greeks))
|
|
788
|
-
ce_greeks = ce_data["greeks"] || ce_data[:greeks]
|
|
789
|
-
sample_greeks[:call] = {
|
|
790
|
-
strike: atm_strike,
|
|
791
|
-
delta: ce_greeks["delta"] || ce_greeks[:delta],
|
|
792
|
-
theta: ce_greeks["theta"] || ce_greeks[:theta],
|
|
793
|
-
gamma: ce_greeks["gamma"] || ce_greeks[:gamma],
|
|
794
|
-
vega: ce_greeks["vega"] || ce_greeks[:vega],
|
|
795
|
-
iv: ce_data["implied_volatility"] || ce_data[:implied_volatility],
|
|
796
|
-
oi: ce_data["oi"] || ce_data[:oi],
|
|
797
|
-
last_price: ce_data["last_price"] || ce_data[:last_price]
|
|
798
|
-
}
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
if pe_data.is_a?(Hash) && (pe_data.key?("greeks") || pe_data.key?(:greeks))
|
|
802
|
-
pe_greeks = pe_data["greeks"] || pe_data[:greeks]
|
|
803
|
-
sample_greeks[:put] = {
|
|
804
|
-
strike: atm_strike,
|
|
805
|
-
delta: pe_greeks["delta"] || pe_greeks[:delta],
|
|
806
|
-
theta: pe_greeks["theta"] || pe_greeks[:theta],
|
|
807
|
-
gamma: pe_greeks["gamma"] || pe_greeks[:gamma],
|
|
808
|
-
vega: pe_greeks["vega"] || pe_greeks[:vega],
|
|
809
|
-
iv: pe_data["implied_volatility"] || pe_data[:implied_volatility],
|
|
810
|
-
oi: pe_data["oi"] || pe_data[:oi],
|
|
811
|
-
last_price: pe_data["last_price"] || pe_data[:last_price]
|
|
812
|
-
}
|
|
813
|
-
end
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
{
|
|
817
|
-
expiry: chain_result[:result][:expiry],
|
|
818
|
-
underlying_last_price: underlying_price,
|
|
819
|
-
strikes_count: strike_prices.length,
|
|
820
|
-
has_call_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("ce") || first_strike_data.key?(:ce)),
|
|
821
|
-
has_put_options: first_strike_data.is_a?(Hash) && (first_strike_data.key?("pe") || first_strike_data.key?(:pe)),
|
|
822
|
-
has_greeks: !sample_greeks.empty?,
|
|
823
|
-
strike_range: if strike_prices.empty?
|
|
824
|
-
nil
|
|
825
|
-
else
|
|
826
|
-
{
|
|
827
|
-
min: strike_prices.first,
|
|
828
|
-
max: strike_prices.last,
|
|
829
|
-
sample_strikes: strike_prices.first(5)
|
|
830
|
-
}
|
|
831
|
-
end,
|
|
832
|
-
sample_greeks: sample_greeks.empty? ? nil : sample_greeks
|
|
833
|
-
}
|
|
834
|
-
else
|
|
835
|
-
{ expiry: chain_result[:result][:expiry], chain_type: chain.class }
|
|
836
|
-
end
|
|
837
|
-
puts " ✅ Option chain retrieved for expiry: #{chain_result[:result][:expiry]}"
|
|
838
|
-
puts " 📊 Underlying LTP: #{underlying_price}" if underlying_price
|
|
839
|
-
puts " 📊 Chain summary: #{JSON.pretty_generate(chain_summary)}"
|
|
840
|
-
elsif chain_result[:error]
|
|
841
|
-
puts " ⚠️ Could not retrieve option chain data: #{chain_result[:error]}"
|
|
842
|
-
end
|
|
885
|
+
handle_option_chain_result(chain_result)
|
|
843
886
|
end
|
|
844
887
|
elsif expiry_list_result[:error]
|
|
845
888
|
puts " ⚠️ #{expiry_list_result[:error]}"
|