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,563 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# DhanHQ Tools - All DhanHQ API operations
|
|
5
|
+
# Contains:
|
|
6
|
+
# - Data APIs (6): Market Quote, Live Market Feed, Full Market Depth,
|
|
7
|
+
# Historical Data, Expired Options Data, Option Chain
|
|
8
|
+
# - Trading Tools: Order parameter building (does not place orders)
|
|
9
|
+
|
|
10
|
+
require "json"
|
|
11
|
+
require "dhan_hq"
|
|
12
|
+
|
|
13
|
+
# Helper to get valid exchange segments from DhanHQ constants
|
|
14
|
+
def valid_exchange_segments
|
|
15
|
+
DhanHQ::Constants::EXCHANGE_SEGMENTS
|
|
16
|
+
rescue StandardError
|
|
17
|
+
["NSE_EQ", "NSE_FNO", "NSE_CURRENCY", "BSE_EQ", "BSE_FNO",
|
|
18
|
+
"BSE_CURRENCY", "MCX_COMM", "IDX_I"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Helper to get INDEX constant
|
|
22
|
+
def index_exchange_segment
|
|
23
|
+
DhanHQ::Constants::INDEX
|
|
24
|
+
rescue StandardError
|
|
25
|
+
"IDX_I"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Helper to extract values from different data structures
|
|
29
|
+
def extract_value(data, keys)
|
|
30
|
+
return nil unless data
|
|
31
|
+
|
|
32
|
+
keys.each do |key|
|
|
33
|
+
if data.is_a?(Hash)
|
|
34
|
+
return data[key] if data.key?(key)
|
|
35
|
+
elsif data.respond_to?(key)
|
|
36
|
+
return data.send(key)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# If data is a simple value and we're looking for it directly
|
|
41
|
+
return data if data.is_a?(Numeric) || data.is_a?(String)
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Helper to safely get instrument attribute (handles missing methods)
|
|
47
|
+
def safe_instrument_attr(instrument, attr_name)
|
|
48
|
+
return nil unless instrument
|
|
49
|
+
|
|
50
|
+
instrument.respond_to?(attr_name) ? instrument.send(attr_name) : nil
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Debug logging helper (if needed)
|
|
56
|
+
def debug_log(location, message, data = {}, hypothesis_id = nil)
|
|
57
|
+
log_entry = {
|
|
58
|
+
sessionId: "debug-session",
|
|
59
|
+
runId: "run1",
|
|
60
|
+
hypothesisId: hypothesis_id,
|
|
61
|
+
location: location,
|
|
62
|
+
message: message,
|
|
63
|
+
data: data,
|
|
64
|
+
timestamp: Time.now.to_f * 1000
|
|
65
|
+
}
|
|
66
|
+
File.open("/home/nemesis/project/ollama-client/.cursor/debug.log", "a") do |f|
|
|
67
|
+
f.puts(log_entry.to_json)
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Ignore logging errors
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# DhanHQ Data Tools - Data APIs only
|
|
74
|
+
# Contains only the 6 Data APIs:
|
|
75
|
+
# 1. Market Quote
|
|
76
|
+
# 2. Live Market Feed (LTP)
|
|
77
|
+
# 3. Full Market Depth
|
|
78
|
+
# 4. Historical Data
|
|
79
|
+
# 5. Expired Options Data
|
|
80
|
+
# 6. Option Chain
|
|
81
|
+
class DhanHQDataTools
|
|
82
|
+
class << self
|
|
83
|
+
# Rate limiting: MarketFeed APIs have a limit of 1 request per second
|
|
84
|
+
# Track last API call time to enforce rate limiting
|
|
85
|
+
@last_marketfeed_call = nil
|
|
86
|
+
@marketfeed_mutex = Mutex.new
|
|
87
|
+
|
|
88
|
+
# Helper to enforce rate limiting for MarketFeed APIs (1 request per second)
|
|
89
|
+
def rate_limit_marketfeed
|
|
90
|
+
@marketfeed_mutex ||= Mutex.new
|
|
91
|
+
@marketfeed_mutex.synchronize do
|
|
92
|
+
if @last_marketfeed_call
|
|
93
|
+
elapsed = Time.now - @last_marketfeed_call
|
|
94
|
+
sleep(1.1 - elapsed) if elapsed < 1.1 # Add 0.1s buffer
|
|
95
|
+
end
|
|
96
|
+
@last_marketfeed_call = Time.now
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 1. Market Quote API - Get market quote using Instrument convenience method
|
|
101
|
+
# Uses instrument.quote which automatically uses instrument's security_id,
|
|
102
|
+
# exchange_segment, and instrument attributes
|
|
103
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol
|
|
104
|
+
# (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
105
|
+
# Rate limit: 1 request per second
|
|
106
|
+
def get_market_quote(exchange_segment:, security_id: nil, symbol: nil)
|
|
107
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
108
|
+
instrument_symbol = symbol || security_id
|
|
109
|
+
unless instrument_symbol
|
|
110
|
+
return {
|
|
111
|
+
action: "get_market_quote",
|
|
112
|
+
error: "Either symbol or security_id must be provided",
|
|
113
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
rate_limit_marketfeed # Enforce rate limiting
|
|
118
|
+
instrument_symbol = instrument_symbol.to_s
|
|
119
|
+
exchange_segment = exchange_segment.to_s
|
|
120
|
+
|
|
121
|
+
# Find instrument first
|
|
122
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
123
|
+
|
|
124
|
+
if instrument
|
|
125
|
+
# Use instrument convenience method - automatically uses instrument's attributes
|
|
126
|
+
# Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
|
|
127
|
+
quote_response = instrument.quote
|
|
128
|
+
|
|
129
|
+
# Extract actual quote data from nested structure
|
|
130
|
+
security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
|
|
131
|
+
if quote_response.is_a?(Hash) && quote_response["data"]
|
|
132
|
+
quote_data = quote_response.dig("data", exchange_segment,
|
|
133
|
+
security_id_str)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
action: "get_market_quote",
|
|
138
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
|
|
139
|
+
result: {
|
|
140
|
+
security_id: safe_instrument_attr(instrument, :security_id) || security_id,
|
|
141
|
+
symbol: instrument_symbol,
|
|
142
|
+
exchange_segment: exchange_segment,
|
|
143
|
+
quote: quote_data || quote_response
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
{
|
|
148
|
+
action: "get_market_quote",
|
|
149
|
+
error: "Instrument not found",
|
|
150
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
{
|
|
155
|
+
action: "get_market_quote",
|
|
156
|
+
error: e.message,
|
|
157
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# 2. Live Market Feed API - Get LTP (Last Traded Price) using Instrument convenience method
|
|
162
|
+
# Uses instrument.ltp which automatically uses instrument's security_id, exchange_segment, and instrument attributes
|
|
163
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
164
|
+
# Rate limit: 1 request per second
|
|
165
|
+
def get_live_ltp(exchange_segment:, security_id: nil, symbol: nil)
|
|
166
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
167
|
+
instrument_symbol = symbol || security_id
|
|
168
|
+
unless instrument_symbol
|
|
169
|
+
return {
|
|
170
|
+
action: "get_live_ltp",
|
|
171
|
+
error: "Either symbol or security_id must be provided",
|
|
172
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
rate_limit_marketfeed # Enforce rate limiting
|
|
177
|
+
instrument_symbol = instrument_symbol.to_s
|
|
178
|
+
exchange_segment = exchange_segment.to_s
|
|
179
|
+
|
|
180
|
+
# Find instrument first
|
|
181
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
182
|
+
|
|
183
|
+
if instrument
|
|
184
|
+
# Use instrument convenience method - automatically uses instrument's attributes
|
|
185
|
+
# Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{"last_price"=>1578.1}}}, "status"=>"success"}
|
|
186
|
+
# OR direct value: 1578.1 (after retry/rate limit handling)
|
|
187
|
+
ltp_response = instrument.ltp
|
|
188
|
+
|
|
189
|
+
# Extract LTP from nested structure or use direct value
|
|
190
|
+
if ltp_response.is_a?(Hash) && ltp_response["data"]
|
|
191
|
+
security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
|
|
192
|
+
ltp_data = ltp_response.dig("data", exchange_segment, security_id_str)
|
|
193
|
+
ltp = extract_value(ltp_data, [:last_price, "last_price"]) if ltp_data
|
|
194
|
+
elsif ltp_response.is_a?(Numeric)
|
|
195
|
+
ltp = ltp_response
|
|
196
|
+
ltp_data = { last_price: ltp }
|
|
197
|
+
else
|
|
198
|
+
ltp = extract_value(ltp_response, [:last_price, "last_price", :ltp, "ltp"]) || ltp_response
|
|
199
|
+
ltp_data = ltp_response
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
action: "get_live_ltp",
|
|
204
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
|
|
205
|
+
result: {
|
|
206
|
+
security_id: safe_instrument_attr(instrument, :security_id) || security_id,
|
|
207
|
+
symbol: instrument_symbol,
|
|
208
|
+
exchange_segment: exchange_segment,
|
|
209
|
+
ltp: ltp,
|
|
210
|
+
ltp_data: ltp_data
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
{
|
|
215
|
+
action: "get_live_ltp",
|
|
216
|
+
error: "Instrument not found",
|
|
217
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
{
|
|
222
|
+
action: "get_live_ltp",
|
|
223
|
+
error: e.message,
|
|
224
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# 3. Full Market Depth API - Get full market depth (bid/ask levels)
|
|
229
|
+
# Uses instrument.quote which automatically uses instrument's security_id,
|
|
230
|
+
# exchange_segment, and instrument attributes
|
|
231
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol
|
|
232
|
+
# (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
233
|
+
# Rate limit: 1 request per second (uses quote API which has stricter limits)
|
|
234
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
235
|
+
def get_market_depth(exchange_segment:, security_id: nil, symbol: nil)
|
|
236
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
237
|
+
instrument_symbol = symbol || security_id
|
|
238
|
+
unless instrument_symbol
|
|
239
|
+
return {
|
|
240
|
+
action: "get_market_depth",
|
|
241
|
+
error: "Either symbol or security_id must be provided",
|
|
242
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
rate_limit_marketfeed # Enforce rate limiting
|
|
247
|
+
instrument_symbol = instrument_symbol.to_s
|
|
248
|
+
exchange_segment = exchange_segment.to_s
|
|
249
|
+
|
|
250
|
+
# Find instrument first
|
|
251
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
252
|
+
|
|
253
|
+
if instrument
|
|
254
|
+
# Use instrument convenience method - automatically uses instrument's attributes
|
|
255
|
+
# Returns nested structure: {"data"=>{"NSE_EQ"=>{"2885"=>{...}}}, "status"=>"success"}
|
|
256
|
+
quote_response = instrument.quote
|
|
257
|
+
|
|
258
|
+
# Extract actual quote data from nested structure
|
|
259
|
+
security_id_str = safe_instrument_attr(instrument, :security_id)&.to_s || security_id.to_s
|
|
260
|
+
if quote_response.is_a?(Hash) && quote_response["data"]
|
|
261
|
+
quote_data = quote_response.dig("data", exchange_segment,
|
|
262
|
+
security_id_str)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Extract market depth (order book) from quote data
|
|
266
|
+
depth = extract_value(quote_data, [:depth, "depth"]) if quote_data
|
|
267
|
+
buy_depth = extract_value(depth, [:buy, "buy"]) if depth
|
|
268
|
+
sell_depth = extract_value(depth, [:sell, "sell"]) if depth
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
action: "get_market_depth",
|
|
272
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
|
|
273
|
+
result: {
|
|
274
|
+
security_id: safe_instrument_attr(instrument, :security_id) || security_id,
|
|
275
|
+
symbol: instrument_symbol,
|
|
276
|
+
exchange_segment: exchange_segment,
|
|
277
|
+
market_depth: quote_data || quote_response,
|
|
278
|
+
# Market depth (order book) - buy and sell sides
|
|
279
|
+
buy_depth: buy_depth,
|
|
280
|
+
sell_depth: sell_depth,
|
|
281
|
+
# Additional quote data
|
|
282
|
+
ltp: quote_data ? extract_value(quote_data, [:last_price, "last_price"]) : nil,
|
|
283
|
+
volume: quote_data ? extract_value(quote_data, [:volume, "volume"]) : nil,
|
|
284
|
+
oi: quote_data ? extract_value(quote_data, [:oi, "oi"]) : nil,
|
|
285
|
+
ohlc: quote_data ? extract_value(quote_data, [:ohlc, "ohlc"]) : nil
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else
|
|
289
|
+
{
|
|
290
|
+
action: "get_market_depth",
|
|
291
|
+
error: "Instrument not found",
|
|
292
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
293
|
+
}
|
|
294
|
+
end
|
|
295
|
+
rescue StandardError => e
|
|
296
|
+
{
|
|
297
|
+
action: "get_market_depth",
|
|
298
|
+
error: e.message,
|
|
299
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
303
|
+
|
|
304
|
+
# 4. Historical Data API - Get historical data using Instrument convenience methods
|
|
305
|
+
# These methods automatically use instrument's security_id, exchange_segment, and instrument attributes
|
|
306
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
307
|
+
# rubocop:disable Metrics/ParameterLists
|
|
308
|
+
def get_historical_data(exchange_segment:, from_date:, to_date:, security_id: nil, symbol: nil, interval: nil,
|
|
309
|
+
expiry_code: nil)
|
|
310
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
311
|
+
instrument_symbol = symbol || security_id
|
|
312
|
+
unless instrument_symbol
|
|
313
|
+
return {
|
|
314
|
+
action: "get_historical_data",
|
|
315
|
+
error: "Either symbol or security_id must be provided",
|
|
316
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
instrument_symbol = instrument_symbol.to_s
|
|
321
|
+
exchange_segment = exchange_segment.to_s
|
|
322
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
323
|
+
|
|
324
|
+
if instrument
|
|
325
|
+
if interval
|
|
326
|
+
# Intraday data - automatically uses instrument's attributes
|
|
327
|
+
data = instrument.intraday(
|
|
328
|
+
from_date: from_date,
|
|
329
|
+
to_date: to_date,
|
|
330
|
+
interval: interval
|
|
331
|
+
)
|
|
332
|
+
{
|
|
333
|
+
action: "get_historical_data",
|
|
334
|
+
type: "intraday",
|
|
335
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
336
|
+
from_date: from_date, to_date: to_date, interval: interval },
|
|
337
|
+
result: {
|
|
338
|
+
data: data,
|
|
339
|
+
count: data.is_a?(Array) ? data.length : 0,
|
|
340
|
+
instrument_info: {
|
|
341
|
+
trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
|
|
342
|
+
instrument_type: safe_instrument_attr(instrument, :instrument_type)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else
|
|
347
|
+
# Daily data - automatically uses instrument's attributes
|
|
348
|
+
# expiry_code is optional for futures/options
|
|
349
|
+
daily_params = { from_date: from_date, to_date: to_date }
|
|
350
|
+
daily_params[:expiry_code] = expiry_code if expiry_code
|
|
351
|
+
data = instrument.daily(**daily_params)
|
|
352
|
+
{
|
|
353
|
+
action: "get_historical_data",
|
|
354
|
+
type: "daily",
|
|
355
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
356
|
+
from_date: from_date, to_date: to_date, expiry_code: expiry_code },
|
|
357
|
+
result: {
|
|
358
|
+
data: data,
|
|
359
|
+
count: data.is_a?(Array) ? data.length : 0,
|
|
360
|
+
instrument_info: {
|
|
361
|
+
trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
|
|
362
|
+
instrument_type: safe_instrument_attr(instrument, :instrument_type)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
end
|
|
367
|
+
else
|
|
368
|
+
{
|
|
369
|
+
action: "get_historical_data",
|
|
370
|
+
error: "Instrument not found",
|
|
371
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
372
|
+
}
|
|
373
|
+
end
|
|
374
|
+
rescue StandardError => e
|
|
375
|
+
{
|
|
376
|
+
action: "get_historical_data",
|
|
377
|
+
error: e.message,
|
|
378
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
# rubocop:enable Metrics/ParameterLists
|
|
382
|
+
|
|
383
|
+
# 6. Option Chain API - Get option chain using Instrument convenience methods
|
|
384
|
+
# These methods automatically use instrument's security_id, exchange_segment, and instrument attributes
|
|
385
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
386
|
+
def get_option_chain(exchange_segment:, security_id: nil, symbol: nil, expiry: nil)
|
|
387
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
388
|
+
instrument_symbol = symbol || security_id
|
|
389
|
+
unless instrument_symbol
|
|
390
|
+
return {
|
|
391
|
+
action: "get_option_chain",
|
|
392
|
+
error: "Either symbol or security_id must be provided",
|
|
393
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
instrument_symbol = instrument_symbol.to_s
|
|
398
|
+
exchange_segment = exchange_segment.to_s
|
|
399
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
400
|
+
|
|
401
|
+
if instrument
|
|
402
|
+
if expiry
|
|
403
|
+
# Get option chain for specific expiry - automatically uses instrument's attributes
|
|
404
|
+
chain = instrument.option_chain(expiry: expiry)
|
|
405
|
+
{
|
|
406
|
+
action: "get_option_chain",
|
|
407
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment, expiry: expiry },
|
|
408
|
+
result: {
|
|
409
|
+
expiry: expiry,
|
|
410
|
+
chain: chain,
|
|
411
|
+
instrument_info: {
|
|
412
|
+
trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
|
|
413
|
+
instrument_type: safe_instrument_attr(instrument, :instrument_type)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else
|
|
418
|
+
# Get list of available expiries - automatically uses instrument's attributes
|
|
419
|
+
expiries = instrument.expiry_list
|
|
420
|
+
{
|
|
421
|
+
action: "get_option_chain",
|
|
422
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment },
|
|
423
|
+
result: {
|
|
424
|
+
expiries: expiries,
|
|
425
|
+
count: expiries.is_a?(Array) ? expiries.length : 0,
|
|
426
|
+
instrument_info: {
|
|
427
|
+
trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
|
|
428
|
+
instrument_type: safe_instrument_attr(instrument, :instrument_type)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
end
|
|
433
|
+
else
|
|
434
|
+
{
|
|
435
|
+
action: "get_option_chain",
|
|
436
|
+
error: "Instrument not found",
|
|
437
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
438
|
+
}
|
|
439
|
+
end
|
|
440
|
+
rescue StandardError => e
|
|
441
|
+
{
|
|
442
|
+
action: "get_option_chain",
|
|
443
|
+
error: e.message,
|
|
444
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment }
|
|
445
|
+
}
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# 5. Expired Options Data API - Get historical expired options data
|
|
449
|
+
# Uses Instrument convenience method which automatically uses instrument's attributes
|
|
450
|
+
# Note: Instrument.find(exchange_segment, symbol) expects symbol (e.g., "NIFTY", "RELIANCE"), not security_id
|
|
451
|
+
def get_expired_options_data(exchange_segment:, expiry_date:, security_id: nil, symbol: nil, expiry_code: nil)
|
|
452
|
+
# Instrument.find expects symbol, support both for backward compatibility
|
|
453
|
+
instrument_symbol = symbol || security_id
|
|
454
|
+
unless instrument_symbol
|
|
455
|
+
return {
|
|
456
|
+
action: "get_expired_options_data",
|
|
457
|
+
error: "Either symbol or security_id must be provided",
|
|
458
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
459
|
+
expiry_date: expiry_date }
|
|
460
|
+
}
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
instrument_symbol = instrument_symbol.to_s
|
|
464
|
+
exchange_segment = exchange_segment.to_s
|
|
465
|
+
instrument = DhanHQ::Models::Instrument.find(exchange_segment, instrument_symbol)
|
|
466
|
+
|
|
467
|
+
if instrument
|
|
468
|
+
# Get historical data for the expiry date - automatically uses instrument's attributes
|
|
469
|
+
daily_params = { from_date: expiry_date, to_date: expiry_date }
|
|
470
|
+
daily_params[:expiry_code] = expiry_code if expiry_code
|
|
471
|
+
expired_data = instrument.daily(**daily_params)
|
|
472
|
+
{
|
|
473
|
+
action: "get_expired_options_data",
|
|
474
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
475
|
+
expiry_date: expiry_date, expiry_code: expiry_code },
|
|
476
|
+
result: {
|
|
477
|
+
security_id: security_id,
|
|
478
|
+
exchange_segment: exchange_segment,
|
|
479
|
+
expiry_date: expiry_date,
|
|
480
|
+
data: expired_data,
|
|
481
|
+
instrument_info: {
|
|
482
|
+
trading_symbol: safe_instrument_attr(instrument, :trading_symbol),
|
|
483
|
+
instrument_type: safe_instrument_attr(instrument, :instrument_type),
|
|
484
|
+
expiry_flag: safe_instrument_attr(instrument, :expiry_flag)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else
|
|
489
|
+
{
|
|
490
|
+
action: "get_expired_options_data",
|
|
491
|
+
error: "Instrument not found",
|
|
492
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
493
|
+
expiry_date: expiry_date }
|
|
494
|
+
}
|
|
495
|
+
end
|
|
496
|
+
rescue StandardError => e
|
|
497
|
+
{
|
|
498
|
+
action: "get_expired_options_data",
|
|
499
|
+
error: e.message,
|
|
500
|
+
params: { security_id: security_id, symbol: symbol, exchange_segment: exchange_segment,
|
|
501
|
+
expiry_date: expiry_date }
|
|
502
|
+
}
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# DhanHQ Trading Tools - Order parameter building only
|
|
508
|
+
class DhanHQTradingTools
|
|
509
|
+
class << self
|
|
510
|
+
# Build order parameters (does not place order)
|
|
511
|
+
def build_order_params(params)
|
|
512
|
+
{
|
|
513
|
+
action: "place_order",
|
|
514
|
+
params: params,
|
|
515
|
+
order_params: {
|
|
516
|
+
transaction_type: params[:transaction_type] || "BUY",
|
|
517
|
+
exchange_segment: params[:exchange_segment] || "NSE_EQ",
|
|
518
|
+
product_type: params[:product_type] || "MARGIN",
|
|
519
|
+
order_type: params[:order_type] || "LIMIT",
|
|
520
|
+
validity: params[:validity] || "DAY",
|
|
521
|
+
security_id: params[:security_id],
|
|
522
|
+
quantity: params[:quantity] || 1,
|
|
523
|
+
price: params[:price]
|
|
524
|
+
},
|
|
525
|
+
message: "Order parameters ready: #{params[:transaction_type]} " \
|
|
526
|
+
"#{params[:quantity]} #{params[:security_id]} @ #{params[:price]}"
|
|
527
|
+
}
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Build super order parameters (does not place order)
|
|
531
|
+
def build_super_order_params(params)
|
|
532
|
+
{
|
|
533
|
+
action: "place_super_order",
|
|
534
|
+
params: params,
|
|
535
|
+
order_params: {
|
|
536
|
+
transaction_type: params[:transaction_type] || "BUY",
|
|
537
|
+
exchange_segment: params[:exchange_segment] || "NSE_EQ",
|
|
538
|
+
product_type: params[:product_type] || "MARGIN",
|
|
539
|
+
order_type: params[:order_type] || "LIMIT",
|
|
540
|
+
security_id: params[:security_id],
|
|
541
|
+
quantity: params[:quantity] || 1,
|
|
542
|
+
price: params[:price],
|
|
543
|
+
target_price: params[:target_price],
|
|
544
|
+
stop_loss_price: params[:stop_loss_price],
|
|
545
|
+
trailing_jump: params[:trailing_jump] || 10
|
|
546
|
+
},
|
|
547
|
+
message: "Super order parameters ready: Entry @ #{params[:price]}, " \
|
|
548
|
+
"SL: #{params[:stop_loss_price]}, TP: #{params[:target_price]}"
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Build cancel order parameters (does not cancel)
|
|
553
|
+
def build_cancel_params(order_id:)
|
|
554
|
+
{
|
|
555
|
+
action: "cancel_order",
|
|
556
|
+
params: { order_id: order_id },
|
|
557
|
+
message: "Cancel parameters ready for order: #{order_id}",
|
|
558
|
+
note: "To actually cancel, call: DhanHQ::Models::Order.find(order_id).cancel"
|
|
559
|
+
}
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example: Ollama structured outputs using chat API
|
|
5
|
+
# This matches the JavaScript example from the Ollama documentation
|
|
6
|
+
|
|
7
|
+
require "json"
|
|
8
|
+
require_relative "../lib/ollama_client"
|
|
9
|
+
|
|
10
|
+
def run(model)
|
|
11
|
+
# Define the JSON schema for friend info
|
|
12
|
+
friend_info_schema = {
|
|
13
|
+
"type" => "object",
|
|
14
|
+
"required" => %w[name age is_available],
|
|
15
|
+
"properties" => {
|
|
16
|
+
"name" => {
|
|
17
|
+
"type" => "string",
|
|
18
|
+
"description" => "The name of the friend"
|
|
19
|
+
},
|
|
20
|
+
"age" => {
|
|
21
|
+
"type" => "integer",
|
|
22
|
+
"description" => "The age of the friend"
|
|
23
|
+
},
|
|
24
|
+
"is_available" => {
|
|
25
|
+
"type" => "boolean",
|
|
26
|
+
"description" => "Whether the friend is available"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Define the schema for friend list
|
|
32
|
+
friend_list_schema = {
|
|
33
|
+
"type" => "object",
|
|
34
|
+
"required" => ["friends"],
|
|
35
|
+
"properties" => {
|
|
36
|
+
"friends" => {
|
|
37
|
+
"type" => "array",
|
|
38
|
+
"description" => "An array of friends",
|
|
39
|
+
"items" => friend_info_schema
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
client = Ollama::Client.new
|
|
44
|
+
|
|
45
|
+
messages = [{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: "I have two friends. The first is Ollama 22 years old busy saving the world, " \
|
|
48
|
+
"and the second is Alonso 23 years old and wants to hang out. " \
|
|
49
|
+
"Return a list of friends in JSON format"
|
|
50
|
+
}]
|
|
51
|
+
|
|
52
|
+
response = client.chat(
|
|
53
|
+
model: model,
|
|
54
|
+
messages: messages,
|
|
55
|
+
format: friend_list_schema,
|
|
56
|
+
allow_chat: true,
|
|
57
|
+
options: {
|
|
58
|
+
temperature: 0 # Make responses more deterministic
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Parse and validate the response (already validated by client, but showing usage)
|
|
63
|
+
begin
|
|
64
|
+
friends_response = response # Already parsed and validated
|
|
65
|
+
puts JSON.pretty_generate(friends_response)
|
|
66
|
+
rescue Ollama::SchemaViolationError => e
|
|
67
|
+
puts "Generated invalid response: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Run with the same model as the JavaScript example
|
|
72
|
+
run("llama3.1:8b")
|