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,843 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/ollama_client"
|
|
5
|
+
require "tty-reader"
|
|
6
|
+
require "tty-screen"
|
|
7
|
+
require "tty-cursor"
|
|
8
|
+
require "dhan_hq"
|
|
9
|
+
require "date"
|
|
10
|
+
require_relative "dhanhq_tools"
|
|
11
|
+
|
|
12
|
+
def build_config
|
|
13
|
+
config = Ollama::Config.new
|
|
14
|
+
config.base_url = ENV["OLLAMA_BASE_URL"] if ENV["OLLAMA_BASE_URL"]
|
|
15
|
+
config.model = ENV["OLLAMA_MODEL"] if ENV["OLLAMA_MODEL"]
|
|
16
|
+
config.temperature = ENV["OLLAMA_TEMPERATURE"].to_f if ENV["OLLAMA_TEMPERATURE"]
|
|
17
|
+
config
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exit_command?(text)
|
|
21
|
+
%w[/exit /quit exit quit].include?(text.downcase)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def system_prompt_from_env
|
|
25
|
+
system_prompt = ENV.fetch("OLLAMA_SYSTEM", nil)
|
|
26
|
+
return nil unless system_prompt && !system_prompt.strip.empty?
|
|
27
|
+
|
|
28
|
+
system_prompt
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def print_banner(config)
|
|
32
|
+
puts "DhanHQ data console"
|
|
33
|
+
puts "Model: #{config.model}"
|
|
34
|
+
puts "Base URL: #{config.base_url}"
|
|
35
|
+
puts "Type /exit to quit."
|
|
36
|
+
puts "Screen: #{TTY::Screen.width}x#{TTY::Screen.height}"
|
|
37
|
+
puts
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
HISTORY_PATH = ".ollama_dhan_history"
|
|
41
|
+
MAX_HISTORY = 200
|
|
42
|
+
COLOR_RESET = "\e[0m"
|
|
43
|
+
COLOR_USER = "\e[32m"
|
|
44
|
+
COLOR_LLM = "\e[36m"
|
|
45
|
+
USER_PROMPT = "#{COLOR_USER}you>#{COLOR_RESET} ".freeze
|
|
46
|
+
LLM_PROMPT = "#{COLOR_LLM}llm>#{COLOR_RESET} ".freeze
|
|
47
|
+
|
|
48
|
+
def build_reader
|
|
49
|
+
TTY::Reader.new
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def read_input(reader)
|
|
53
|
+
reader.read_line(USER_PROMPT)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_history(reader, path)
|
|
57
|
+
history = load_history_list(path)
|
|
58
|
+
history.reverse_each { |line| reader.add_to_history(line) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load_history_list(path)
|
|
62
|
+
return [] unless File.exist?(path)
|
|
63
|
+
|
|
64
|
+
unique_history(normalize_history(File.readlines(path, chomp: true)))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_history(lines)
|
|
68
|
+
lines.map(&:strip).reject(&:empty?)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def unique_history(lines)
|
|
72
|
+
seen = {}
|
|
73
|
+
lines.each_with_object([]) do |line, unique|
|
|
74
|
+
next if seen[line]
|
|
75
|
+
|
|
76
|
+
unique << line
|
|
77
|
+
seen[line] = true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_history(path, text)
|
|
82
|
+
history = load_history_list(path)
|
|
83
|
+
history.delete(text)
|
|
84
|
+
history.unshift(text)
|
|
85
|
+
history = history.first(MAX_HISTORY)
|
|
86
|
+
|
|
87
|
+
File.write(path, history.join("\n") + (history.empty? ? "" : "\n"))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def configure_dhanhq!
|
|
91
|
+
DhanHQ.configure_with_env
|
|
92
|
+
puts "✅ DhanHQ configured"
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
puts "❌ DhanHQ configuration error: #{e.message}"
|
|
95
|
+
puts " Make sure CLIENT_ID and ACCESS_TOKEN are set in ENV"
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def tool_system_prompt
|
|
100
|
+
<<~PROMPT
|
|
101
|
+
You are a market data assistant. Use tools to answer user queries completely.
|
|
102
|
+
|
|
103
|
+
CRITICAL: You are an EXECUTOR, not an INSTRUCTOR. When the user asks for data, you must EXECUTE tool calls to get that data, not describe how to get it.
|
|
104
|
+
Your job is to actually call the tools and provide the results, not to explain how the tools work.
|
|
105
|
+
DO NOT provide step-by-step instructions - actually execute the steps by making tool calls.
|
|
106
|
+
DO NOT show JSON examples - actually make the tool calls.
|
|
107
|
+
|
|
108
|
+
Available tools with REQUIRED parameters:
|
|
109
|
+
|
|
110
|
+
1. find_instrument(symbol: String) - REQUIRED: symbol
|
|
111
|
+
- Find instrument details (exchange_segment, security_id) by symbol
|
|
112
|
+
- Use this FIRST when you only have a symbol and need to resolve it
|
|
113
|
+
- Returns: exchange_segment, security_id (numeric), trading_symbol, instrument_type
|
|
114
|
+
|
|
115
|
+
2. get_market_quote(exchange_segment: String, symbol: String, security_id: Integer) - REQUIRED: exchange_segment, (symbol OR security_id)
|
|
116
|
+
- Get full market quote (OHLC, depth, volume, etc.)
|
|
117
|
+
- exchange_segment MUST be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
118
|
+
- symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE")
|
|
119
|
+
- security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
120
|
+
- Rate limit: 1 request per second
|
|
121
|
+
- Up to 1000 instruments per request
|
|
122
|
+
|
|
123
|
+
3. get_live_ltp(exchange_segment: String, symbol: String, security_id: Integer) - REQUIRED: exchange_segment, (symbol OR security_id)
|
|
124
|
+
- Get Last Traded Price (LTP) - fastest API for current price
|
|
125
|
+
- exchange_segment MUST be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
126
|
+
- symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE")
|
|
127
|
+
- security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
128
|
+
- Rate limit: 1 request per second
|
|
129
|
+
- Up to 1000 instruments per request
|
|
130
|
+
|
|
131
|
+
4. get_market_depth(exchange_segment: String, symbol: String, security_id: Integer) - REQUIRED: exchange_segment, (symbol OR security_id)
|
|
132
|
+
- Get full market depth (bid/ask levels, order book)
|
|
133
|
+
- exchange_segment MUST be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
134
|
+
- symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE")
|
|
135
|
+
- security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
136
|
+
- Rate limit: 1 request per second
|
|
137
|
+
- Up to 1000 instruments per request
|
|
138
|
+
|
|
139
|
+
5. get_historical_data(exchange_segment: String, from_date: String, to_date: String, symbol: String, security_id: Integer, interval: String, expiry_code: Integer, instrument: String, calculate_indicators: Boolean) - REQUIRED: exchange_segment, from_date, to_date, (symbol OR security_id)
|
|
140
|
+
- Get historical price data (OHLCV) OR technical indicators
|
|
141
|
+
- REQUIRED parameters:
|
|
142
|
+
* exchange_segment: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
143
|
+
* from_date: YYYY-MM-DD format - MUST be provided by user or calculated from user's request
|
|
144
|
+
* to_date: YYYY-MM-DD format (non-inclusive) - MUST be provided by user or calculated from user's request
|
|
145
|
+
* symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE") OR
|
|
146
|
+
* security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
147
|
+
- OPTIONAL parameters:
|
|
148
|
+
* interval: "1", "5", "15", "25", "60" (for intraday) - if provided, returns intraday data; if omitted, returns daily data
|
|
149
|
+
- CRITICAL FOR INTRADAY: If user asks for "intraday", "intraday movement", "intraday range", "today's movement", or similar:
|
|
150
|
+
* You MUST provide the interval parameter
|
|
151
|
+
* DEFAULT INTERVAL: Use "5" (5-minute) or "15" (15-minute) intervals - these are less noisy than 1-minute data
|
|
152
|
+
- Prefer "5" for most intraday analysis (good balance of detail and noise reduction)
|
|
153
|
+
- Use "15" for smoother, less noisy data
|
|
154
|
+
- Only use "1" if user explicitly requests 1-minute data
|
|
155
|
+
* DATE RULES FOR INTRADAY:
|
|
156
|
+
- to_date: MUST be TODAY's date: "#{Date.today.strftime("%Y-%m-%d")}"
|
|
157
|
+
- from_date: Can go back up to 30 days from today (max 30 days back)
|
|
158
|
+
* If user doesn't specify dates: Use from_date = "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
159
|
+
* If user asks for "today's movement": Use from_date = "#{Date.today.strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
160
|
+
* Maximum range: 30 days back from today
|
|
161
|
+
* "Intraday movement" or "intraday range" means the high-low price range for a specific day (usually today)
|
|
162
|
+
* Example: User asks "find the range of intraday movement for NIFTY" → Use interval: "5" (or "15"), from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{Date.today.strftime("%Y-%m-%d")}"
|
|
163
|
+
* Maximum 90 days of intraday data can be fetched at once
|
|
164
|
+
* expiry_code: 0 (far month), 1 (near month), 2 (current month) - for derivatives
|
|
165
|
+
* instrument: EQUITY, INDEX, FUTIDX, FUTSTK, OPTIDX, OPTSTK, FUTCOM, OPTFUT, FUTCUR, OPTCUR
|
|
166
|
+
* calculate_indicators: true/false (default: false) - If true, calculates and returns only technical indicators instead of raw data
|
|
167
|
+
When true, returns: RSI, MACD, SMA20, SMA50, EMA12, EMA26, Bollinger Bands, ATR, price range, volume stats
|
|
168
|
+
This significantly reduces response size and provides ready-to-use indicator values for analysis
|
|
169
|
+
- CRITICAL DATE RULES:
|
|
170
|
+
* NEVER invent or guess dates - dates MUST come from the user's explicit request OR use recent/current dates as default
|
|
171
|
+
* DEFAULT BEHAVIOR: If user doesn't specify dates, use RECENT/CURRENT dates (not old dates):
|
|
172
|
+
- For DAILY data (no interval): Use last 30 days from today (most recent data)
|
|
173
|
+
Example: If today is #{Date.today.strftime("%Y-%m-%d")}, use from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{(Date.today + 1).strftime("%Y-%m-%d")}"
|
|
174
|
+
- For INTRADAY data (with interval): to_date = TODAY's date, from_date can go back up to 30 days
|
|
175
|
+
Example: If today is #{Date.today.strftime("%Y-%m-%d")}, use from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{Date.today.strftime("%Y-%m-%d")}"
|
|
176
|
+
For "today's movement" specifically: from_date = "#{Date.today.strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
177
|
+
- This provides current/recent market data which is most useful for analysis
|
|
178
|
+
* ONLY use past/old dates if user explicitly requests them:
|
|
179
|
+
- If user says "January 2024" or "2024 data", then use those old dates
|
|
180
|
+
- If user says "historical data from 2023", then use 2023 dates
|
|
181
|
+
- But if user just says "historical data" without specifying, use RECENT dates (last 30 days)
|
|
182
|
+
* Relative time references:
|
|
183
|
+
- "last 30 days" → calculate from today backwards 30 days
|
|
184
|
+
- "last week" → calculate the actual dates for the past 7 days
|
|
185
|
+
- "current month" → use dates for the current month
|
|
186
|
+
- "this month" → use dates for the current month
|
|
187
|
+
* NEVER use random old dates like "2024-01-08" or "2024-02-07" unless the user explicitly requested those specific dates
|
|
188
|
+
* PREFER RECENT DATES: When in doubt, use recent dates (last 30 days) rather than old dates
|
|
189
|
+
- Notes: Maximum 90 days for intraday, data available for last 5 years
|
|
190
|
+
|
|
191
|
+
6a. get_expiry_list(exchange_segment: String, symbol: String, security_id: Integer) - REQUIRED: exchange_segment, (symbol OR security_id)
|
|
192
|
+
- Get list of available expiry dates for an underlying instrument
|
|
193
|
+
- REQUIRED parameters:
|
|
194
|
+
* exchange_segment: For indices use "IDX_I", for stocks use "NSE_FNO" or "BSE_FNO"
|
|
195
|
+
* symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE") OR
|
|
196
|
+
* security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
197
|
+
- Returns: Array of available expiry dates in "YYYY-MM-DD" format
|
|
198
|
+
- For indices (NIFTY, BANKNIFTY): Use exchange_segment: "IDX_I" and INTEGER security_id from find_instrument (e.g., 13, not "NIFTY")
|
|
199
|
+
- Rate limit: 1 request per 3 seconds
|
|
200
|
+
- Use this tool first to get available expiries before calling get_option_chain
|
|
201
|
+
|
|
202
|
+
6. get_option_chain(exchange_segment: String, symbol: String, security_id: Integer, expiry: String, strikes_count: Integer) - REQUIRED: exchange_segment, (symbol OR security_id), expiry
|
|
203
|
+
- Get option chain for an underlying instrument for a specific expiry
|
|
204
|
+
- REQUIRED parameters:
|
|
205
|
+
* exchange_segment: For indices use "IDX_I", for stocks use "NSE_FNO" or "BSE_FNO"
|
|
206
|
+
* symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE") OR
|
|
207
|
+
* security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
208
|
+
* expiry: YYYY-MM-DD format (REQUIRED - use get_expiry_list first to get available expiry dates)
|
|
209
|
+
- OPTIONAL parameters:
|
|
210
|
+
* strikes_count: Number of strikes to return around ATM (default: 5)
|
|
211
|
+
- 3 strikes: 1 ITM, ATM, 1 OTM (minimal overview)
|
|
212
|
+
- 5 strikes: 2 ITM, ATM, 2 OTM (recommended default - good for analysis)
|
|
213
|
+
- 7 strikes: 3 ITM, ATM, 3 OTM (more detailed analysis)
|
|
214
|
+
- 10+ strikes: More comprehensive view (larger response)
|
|
215
|
+
- Returns: Option chain filtered to show strikes around ATM (ITM, ATM, OTM)
|
|
216
|
+
- Note: Chain is automatically filtered to reduce response size - only strikes with both CE and PE are included
|
|
217
|
+
- For indices (NIFTY, BANKNIFTY): Use exchange_segment: "IDX_I" and INTEGER security_id from find_instrument (e.g., 13, not "NIFTY")
|
|
218
|
+
- Rate limit: 1 request per 3 seconds
|
|
219
|
+
- CRITICAL WORKFLOW: If user asks for "next expiry", "upcoming expiry", "nearest expiry", or similar:
|
|
220
|
+
1. First call get_expiry_list to get the expiry list
|
|
221
|
+
2. Extract the FIRST expiry date from result.expiries array (this is the next/upcoming expiry)
|
|
222
|
+
3. Use the EXACT expiry date string from result.expiries[0] - DO NOT invent, guess, or modify the date
|
|
223
|
+
4. Call get_option_chain with that EXACT expiry date to get the actual option chain data
|
|
224
|
+
5. Do NOT stop after getting the expiry list - you MUST fetch the actual chain data
|
|
225
|
+
- Example: User asks "option chain of NIFTY for next expiry"
|
|
226
|
+
Step 1: get_expiry_list(exchange_segment: "IDX_I", security_id: 13) → returns {result: {expiries: ["2026-01-20", "2026-01-27", ...]}}
|
|
227
|
+
Step 2: get_option_chain(exchange_segment: "IDX_I", security_id: 13, expiry: "2026-01-20") → use the EXACT first date from the list
|
|
228
|
+
- CRITICAL: If the expiry list shows ["2026-01-20", ...], use "2026-01-20", NOT "2024-12-26" or any other date you might think is correct
|
|
229
|
+
- NEVER invent or guess expiry dates - ALWAYS copy the exact date string from result.expiries array
|
|
230
|
+
|
|
231
|
+
7. get_expired_options_data(exchange_segment: String, expiry_date: String, symbol: String, security_id: Integer, interval: String, instrument: String, expiry_flag: String, expiry_code: Integer, strike: String, drv_option_type: String, required_data: Array) - REQUIRED: exchange_segment, expiry_date, (symbol OR security_id)
|
|
232
|
+
- Get historical expired options data
|
|
233
|
+
- REQUIRED parameters:
|
|
234
|
+
* exchange_segment: NSE_FNO, BSE_FNO, NSE_EQ, BSE_EQ
|
|
235
|
+
* expiry_date: YYYY-MM-DD format
|
|
236
|
+
* symbol: Trading symbol string (e.g., "NIFTY", "RELIANCE") OR
|
|
237
|
+
* security_id: MUST be an INTEGER (e.g., 13, 2885) - NEVER use a symbol string as security_id
|
|
238
|
+
- OPTIONAL parameters (with defaults):
|
|
239
|
+
* interval: "1", "5", "15", "25", "60" (default: "1")
|
|
240
|
+
* instrument: "OPTIDX" (Index Options) or "OPTSTK" (Stock Options) - auto-detected if not provided
|
|
241
|
+
* expiry_flag: "WEEK" or "MONTH" (default: "MONTH")
|
|
242
|
+
* expiry_code: 0 (far), 1 (near), 2 (current) - default: 1 (near month)
|
|
243
|
+
* strike: "ATM", "ATM+X", "ATM-X" (default: "ATM") - up to ATM+10/ATM-10 for index options, ATM+3/ATM-3 for others
|
|
244
|
+
* drv_option_type: "CALL" or "PUT" (default: "CALL")
|
|
245
|
+
* required_data: Array of fields like ["open", "high", "low", "close", "iv", "volume", "strike", "oi", "spot"] (default: all fields)
|
|
246
|
+
- Notes: Maximum 31 days of data per request, historical data available for last 5 years
|
|
247
|
+
|
|
248
|
+
EXCHANGE SEGMENTS (valid values):
|
|
249
|
+
- IDX_I - Index
|
|
250
|
+
- NSE_EQ - NSE Equity Cash
|
|
251
|
+
- NSE_FNO - NSE Futures & Options
|
|
252
|
+
- NSE_CURRENCY - NSE Currency
|
|
253
|
+
- BSE_EQ - BSE Equity Cash
|
|
254
|
+
- BSE_FNO - BSE Futures & Options
|
|
255
|
+
- BSE_CURRENCY - BSE Currency
|
|
256
|
+
- MCX_COMM - MCX Commodity
|
|
257
|
+
|
|
258
|
+
INSTRUMENT TYPES (valid values):
|
|
259
|
+
- EQUITY - Equity
|
|
260
|
+
- INDEX - Index
|
|
261
|
+
- FUTIDX - Futures Index
|
|
262
|
+
- FUTSTK - Futures Stock
|
|
263
|
+
- OPTIDX - Options Index
|
|
264
|
+
- OPTSTK - Options Stock
|
|
265
|
+
- FUTCOM - Futures Commodity
|
|
266
|
+
- OPTFUT - Options Futures
|
|
267
|
+
- FUTCUR - Futures Currency
|
|
268
|
+
- OPTCUR - Options Currency
|
|
269
|
+
|
|
270
|
+
WORKFLOW:
|
|
271
|
+
- CRITICAL: You are an EXECUTOR, not an INSTRUCTOR. You must EXECUTE tool calls, not describe how to use them.
|
|
272
|
+
- When you need to call multiple tools (e.g., find_instrument then get_live_ltp), call them in sequence.
|
|
273
|
+
- After each tool call, use the EXACT values from the tool result to continue. Do not stop until you have fully answered the user's query.
|
|
274
|
+
- CRITICAL: You MUST continue making tool calls until you get results and can fully answer the user's query.
|
|
275
|
+
* Do NOT stop after describing what you would do - you MUST actually do it
|
|
276
|
+
* Do NOT stop after showing a JSON example - you MUST actually make the tool call
|
|
277
|
+
* Keep making tool calls until you have the data needed to answer the user's question
|
|
278
|
+
* After getting results, use them to provide a complete answer to the user
|
|
279
|
+
* The user's query is NOT answered until you provide the actual data/answer, not just instructions
|
|
280
|
+
* If a tool call fails, immediately make another tool call with corrected parameters - do NOT stop
|
|
281
|
+
* Continue the tool calling loop until you have successfully retrieved data and can answer the user
|
|
282
|
+
* Example: User asks "find range of intraday movement" → Call find_instrument → Call get_historical_data → Calculate range from results → Provide answer with actual range value
|
|
283
|
+
- CRITICAL: You MUST actually execute tool calls, not just describe them. The system will automatically execute any tool calls you make.
|
|
284
|
+
* If you describe a tool call instead of making it, the user's question will not be answered
|
|
285
|
+
* Make the tool call, wait for results, then use those results to provide a complete answer
|
|
286
|
+
* If a tool call fails, fix the parameters and ACTUALLY retry - do NOT just describe the fix
|
|
287
|
+
- CRITICAL: NEVER invent, guess, or modify values from tool results. ALWAYS use the exact values as returned.
|
|
288
|
+
- If you called find_instrument and got a result like: {"result": {"exchange_segment": "IDX_I", "security_id": "13", "symbol": "NIFTY"}}
|
|
289
|
+
Then for the next tool call, use:
|
|
290
|
+
- exchange_segment: "IDX_I" (the exact string from result.exchange_segment)
|
|
291
|
+
- security_id: 13 (the INTEGER value from result.security_id - convert string "13" to integer 13)
|
|
292
|
+
- Do NOT use the symbol string "NIFTY" as security_id - security_id must be an INTEGER
|
|
293
|
+
- Example: If find_instrument returns {"result": {"exchange_segment": "IDX_I", "security_id": "13", "symbol": "NIFTY"}}
|
|
294
|
+
Then call get_option_chain with: exchange_segment: "IDX_I", security_id: 13 (integer, NOT the string "NIFTY" or "13")
|
|
295
|
+
- CRITICAL: security_id must ALWAYS be an INTEGER (e.g., 13, 2885), NEVER a symbol string (e.g., "NIFTY", "RELIANCE")
|
|
296
|
+
- CRITICAL: If result.security_id is a string (e.g., "13"), convert it to integer (13) before using as security_id parameter
|
|
297
|
+
- CRITICAL: If get_expiry_list returns {"result": {"expiries": ["2026-01-20", "2026-01-27", ...]}}, use "2026-01-20" (the first date), NOT "2024-12-26" or any other date
|
|
298
|
+
- NEVER invent dates, expiry values, or any other data - ALWAYS use exact values from tool results
|
|
299
|
+
- CRITICAL FOR OPTION CHAIN: If user asks for "next expiry", "upcoming expiry", "nearest expiry", or "option chain" without specifying expiry:
|
|
300
|
+
1. First call get_expiry_list to get the list of available expiry dates
|
|
301
|
+
2. Extract the FIRST expiry date from result.expiries array (this is the next/upcoming expiry)
|
|
302
|
+
3. Use the EXACT expiry date string from result.expiries[0] - DO NOT invent, guess, or modify the date
|
|
303
|
+
4. Call get_option_chain with that EXACT expiry date to get the actual option chain data
|
|
304
|
+
5. DO NOT stop after getting just the expiry list - you MUST fetch the actual chain data to answer the user's query
|
|
305
|
+
6. Only provide your final answer after you have the actual option chain data, not just the expiry list
|
|
306
|
+
7. CRITICAL EXAMPLE: If get_expiry_list returns {"result": {"expiries": ["2026-01-20", "2026-01-27", ...]}},
|
|
307
|
+
you MUST use "2026-01-20" (the first date in the array), NOT "2024-12-26" or any other date
|
|
308
|
+
8. NEVER invent or guess expiry dates - ALWAYS copy the exact date string from result.expiries array
|
|
309
|
+
9. If the expiry list shows dates starting in 2026, use a 2026 date, NOT a 2024 date
|
|
310
|
+
- Only provide your final answer after you have all the data needed to answer the user's question.
|
|
311
|
+
- Indices like NIFTY, BANKNIFTY DO have options - they are index options. Use exchange_segment: IDX_I and the numeric security_id from find_instrument.
|
|
312
|
+
|
|
313
|
+
CRITICAL RULES:
|
|
314
|
+
- If the user provides ONLY a symbol (e.g., "RELIANCE", "TCS", "ltp of RELIANCE") WITHOUT exchange_segment, you MUST call find_instrument FIRST to get the correct exchange_segment and security_id.
|
|
315
|
+
- If the user ALREADY provides exchange_segment (e.g., "get_market_quote for RELIANCE on exchange_segment NSE_EQ"), you can call the data tool directly - find_instrument is NOT needed.
|
|
316
|
+
- NEVER guess or invent exchange_segment values. Valid values are: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
317
|
+
- NEVER use "NSE" or "BSE" alone - they are invalid. Always use full values like "NSE_EQ" or "BSE_EQ".
|
|
318
|
+
- Common indices (NIFTY, BANKNIFTY, FINNIFTY, etc.) are found in IDX_I, not NSE_EQ.
|
|
319
|
+
- DATE FORMATS: Always use YYYY-MM-DD format (e.g., "#{Date.today.strftime("%Y-%m-%d")}", not "01/08/2024" or "08-Jan-2024")
|
|
320
|
+
- TECHNICAL ANALYSIS: For technical analysis requests, use get_historical_data with calculate_indicators: true
|
|
321
|
+
This returns only calculated indicator values (RSI, MACD, SMA, EMA, Bollinger Bands, ATR) instead of raw data
|
|
322
|
+
This makes the response much smaller and easier to analyze
|
|
323
|
+
Example: get_historical_data(exchange_segment: "NSE_EQ", symbol: "RELIANCE", from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{(Date.today + 1).strftime("%Y-%m-%d")}", calculate_indicators: true)
|
|
324
|
+
- For historical data, to_date is NON-INCLUSIVE (end date is not included in results)
|
|
325
|
+
- CRITICAL DATE HANDLING FOR HISTORICAL DATA:
|
|
326
|
+
* NEVER invent, guess, or randomly choose dates for get_historical_data
|
|
327
|
+
* DEFAULT: Use RECENT/CURRENT dates when user doesn't specify dates:
|
|
328
|
+
- For DAILY data (no interval): Use last 30 days from today
|
|
329
|
+
- For INTRADAY data (with interval): Use TODAY's date (from_date = today, to_date = today + 1)
|
|
330
|
+
* ONLY use past/old dates if user explicitly requests them (e.g., "January 2024", "2023 data", "data from 2022")
|
|
331
|
+
* Date calculation rules:
|
|
332
|
+
* If user doesn't specify dates:
|
|
333
|
+
- For DAILY data (no interval): Use last 30 days from today (RECENT data)
|
|
334
|
+
Example: If today is #{Date.today.strftime("%Y-%m-%d")}, use from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{(Date.today + 1).strftime("%Y-%m-%d")}"
|
|
335
|
+
- For INTRADAY data (with interval): to_date = TODAY's date, from_date can go back up to 30 days (max)
|
|
336
|
+
Example: If today is #{Date.today.strftime("%Y-%m-%d")}, use from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{Date.today.strftime("%Y-%m-%d")}"
|
|
337
|
+
For "today's movement" specifically: from_date = "#{Date.today.strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
338
|
+
Maximum range: 30 days back from today
|
|
339
|
+
* If user says "last 30 days" → Calculate: from_date = today - 30 days, to_date = today + 1 day (since to_date is non-inclusive)
|
|
340
|
+
* If user says "last week" → Calculate the actual dates for the past 7 days
|
|
341
|
+
* If user says "current month" or "this month" → Use dates for the current month
|
|
342
|
+
* If user says "January 2024" (old date) → Use from_date: "2024-01-01", to_date: "2024-02-01" (only if explicitly requested)
|
|
343
|
+
* If user says "past month" → Calculate from today backwards 30 days (recent, not old)
|
|
344
|
+
* CRITICAL FOR INTRADAY: When user asks for "intraday movement", "intraday range", "today's movement", or similar:
|
|
345
|
+
- You MUST provide interval parameter (prefer "5" or "15" for less noisy data, only use "1" if explicitly requested)
|
|
346
|
+
- to_date: MUST be TODAY's date: "#{Date.today.strftime("%Y-%m-%d")}"
|
|
347
|
+
- from_date: Can go back up to 30 days from today (max 30 days back)
|
|
348
|
+
* Default: from_date = "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
349
|
+
* For "today's movement": from_date = "#{Date.today.strftime("%Y-%m-%d")}", to_date = "#{Date.today.strftime("%Y-%m-%d")}"
|
|
350
|
+
- Do NOT use old dates like "2023-01-01" unless user explicitly requests them
|
|
351
|
+
* PREFER RECENT DATES: When user says "historical data" without dates, use last 30 days (recent), NOT old dates like "2024-01-08"
|
|
352
|
+
* NEVER use random old dates like "2024-01-08" to "2024-02-07" unless the user explicitly requested those specific dates
|
|
353
|
+
* Example: If today is #{Date.today.strftime("%Y-%m-%d")}:
|
|
354
|
+
- User says "historical data for RELIANCE" → Use last 30 days: from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{(Date.today + 1).strftime("%Y-%m-%d")}" (RECENT)
|
|
355
|
+
- User says "historical data for RELIANCE from January 2024" → Use old dates: from_date: "2024-01-01", to_date: "2024-02-01" (OLD, explicitly requested)
|
|
356
|
+
- ERROR HANDLING: If a tool returns an error about invalid exchange_segment or missing required parameters, you MUST:
|
|
357
|
+
1. Call find_instrument(symbol) to get the correct exchange_segment and security_id - ACTUALLY call it, don't describe it
|
|
358
|
+
2. Verify all required parameters are provided with correct formats
|
|
359
|
+
3. ACTUALLY RETRY the original tool call with the resolved parameters - do NOT just describe what you would do
|
|
360
|
+
4. After the retry succeeds, use the results to answer the user's question completely
|
|
361
|
+
5. If an error occurs, fix the parameters and ACTUALLY call the tool again - do NOT stop after describing the fix
|
|
362
|
+
6. CRITICAL: When you see an error, you MUST make another tool call to fix it - do NOT output text describing what you would do
|
|
363
|
+
7. CONTINUE making tool calls until you get successful results - do NOT stop after one failed attempt
|
|
364
|
+
8. After getting successful results, use them to calculate/derive the answer and provide it to the user
|
|
365
|
+
9. Example of WRONG behavior: "Here is how you can do it: [JSON example]" - this does NOT execute the tool and does NOT answer the user
|
|
366
|
+
10. Example of CORRECT behavior: Actually make the tool call, get results, calculate the answer from results, then provide the answer to the user
|
|
367
|
+
- CRITICAL: When you need to call a tool, you MUST actually CALL it using the tool_calls mechanism.
|
|
368
|
+
* YOU ARE AN EXECUTOR, NOT AN INSTRUCTOR - execute tool calls, do not describe them
|
|
369
|
+
* DO NOT just describe what you would call in text
|
|
370
|
+
* DO NOT output JSON code blocks showing tool calls
|
|
371
|
+
* DO NOT say "Here is the corrected code:" or "We can call the tool with..." or "Here is how you can do it:"
|
|
372
|
+
* DO NOT show example JSON - the system handles tool calls automatically
|
|
373
|
+
* DO NOT provide step-by-step instructions - actually execute the steps
|
|
374
|
+
* DO NOT say "1. Call find_instrument..." - actually CALL find_instrument
|
|
375
|
+
* YOU MUST actually make the tool call - the system will execute it automatically
|
|
376
|
+
* After the tool executes, use the results to answer the user's question completely
|
|
377
|
+
* Do NOT stop after describing what you would do - actually DO it
|
|
378
|
+
* If a tool call fails, fix the parameters and ACTUALLY call it again - do NOT just describe the fix
|
|
379
|
+
* Your response should contain the actual tool calls, not descriptions of tool calls
|
|
380
|
+
* CONTINUE making tool calls until you have the data needed to answer the user's query
|
|
381
|
+
* Do NOT stop until you have provided the actual answer/data to the user, not just instructions on how to get it
|
|
382
|
+
* The tool calling loop should continue until you have successfully retrieved data and calculated the answer
|
|
383
|
+
* Example workflow: User asks "find range" → Tool call 1 (find_instrument) → Tool call 2 (get_historical_data) → Calculate range from data → Provide answer: "The intraday range is X to Y"
|
|
384
|
+
* REMEMBER: If you describe tool calls instead of making them, the user's question will NOT be answered
|
|
385
|
+
- Only call a tool when the user explicitly asks for market data, prices, quotes, option chains, or historical data.
|
|
386
|
+
- If the user is greeting, chatting, or asking a general question, respond normally without calling tools.
|
|
387
|
+
- When tools are used, fetch real data; do not invent values.
|
|
388
|
+
PROMPT
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def planning_system_prompt
|
|
392
|
+
<<~PROMPT
|
|
393
|
+
You are a planning assistant for a market data console.
|
|
394
|
+
Analyze the user query and decide if tools are required to answer it.
|
|
395
|
+
|
|
396
|
+
CRITICAL: If the query mentions any of these keywords or patterns, tools ARE needed:
|
|
397
|
+
- "get_market_quote", "market quote", "quote"
|
|
398
|
+
- "get_live_ltp", "LTP", "last traded price", "current price"
|
|
399
|
+
- "get_market_depth", "market depth", "depth"
|
|
400
|
+
- "get_historical_data", "historical", "price history", "candles", "OHLC", "intraday", "intraday movement", "intraday range", "today's movement"
|
|
401
|
+
- "get_option_chain", "option chain", "options chain"
|
|
402
|
+
- "get_expired_options", "expired options"
|
|
403
|
+
- Any stock symbol (RELIANCE, TCS, INFY, etc.) with a request for data
|
|
404
|
+
- Any request for real-time or historical market data
|
|
405
|
+
|
|
406
|
+
Examples that NEED tools:
|
|
407
|
+
- "get_market_quote for RELIANCE" → needs_tools: true, tools: [find_instrument, get_market_quote] (no exchange_segment provided)
|
|
408
|
+
- "get_market_quote for RELIANCE on exchange_segment NSE_EQ" → needs_tools: true, tools: [get_market_quote] (exchange_segment already provided, skip find_instrument)
|
|
409
|
+
- "What is the LTP of TCS?" → needs_tools: true, tools: [find_instrument, get_live_ltp] (no exchange_segment provided)
|
|
410
|
+
- "ltp of RELIANCE" → needs_tools: true, tools: [find_instrument, get_live_ltp] (no exchange_segment provided)
|
|
411
|
+
- "Show me historical data for INFY from 2024-01-01 to 2024-01-31" → needs_tools: true, tools: [find_instrument, get_historical_data] (user provided explicit old dates - use them)
|
|
412
|
+
- "Show me last 30 days of RELIANCE" → needs_tools: true, tools: [find_instrument, get_historical_data] (calculate dates from today: from_date = "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date = "#{(Date.today + 1).strftime("%Y-%m-%d")}")
|
|
413
|
+
- "Get historical data for NIFTY" → needs_tools: true, tools: [find_instrument, get_historical_data] (use DEFAULT: last 30 days from today - RECENT dates, not old dates)
|
|
414
|
+
- "Historical data for RELIANCE" → needs_tools: true, tools: [find_instrument, get_historical_data] (use DEFAULT: last 30 days - RECENT dates)
|
|
415
|
+
- "Find the range of intraday movement for NIFTY" → needs_tools: true, tools: [find_instrument, get_historical_data] (MUST use interval: "5" or "15" - prefer "5" for less noise, from_date: "#{(Date.today - 30).strftime("%Y-%m-%d")}", to_date: "#{Date.today.strftime("%Y-%m-%d")}" - up to 30 days back, ending today)
|
|
416
|
+
- "Today's intraday movement for RELIANCE" → needs_tools: true, tools: [find_instrument, get_historical_data] (MUST use interval: "5" or "15" - prefer "5", from_date: "#{Date.today.strftime("%Y-%m-%d")}", to_date: "#{Date.today.strftime("%Y-%m-%d")}" - today only)
|
|
417
|
+
- "Get data from January 2024 for NIFTY" → needs_tools: true, tools: [find_instrument, get_historical_data] (user explicitly requested old dates - use "2024-01-01" to "2024-02-01")
|
|
418
|
+
- "Get option chain for NIFTY" → needs_tools: true, tools: [find_instrument, get_option_chain] (no exchange_segment provided)
|
|
419
|
+
|
|
420
|
+
REQUIRED PARAMETERS SUMMARY:
|
|
421
|
+
- find_instrument: REQUIRED - symbol
|
|
422
|
+
- get_market_quote: REQUIRED - exchange_segment, (symbol OR security_id)
|
|
423
|
+
- get_live_ltp: REQUIRED - exchange_segment, (symbol OR security_id)
|
|
424
|
+
- get_market_depth: REQUIRED - exchange_segment, (symbol OR security_id)
|
|
425
|
+
- get_historical_data: REQUIRED - exchange_segment, from_date (YYYY-MM-DD), to_date (YYYY-MM-DD), (symbol OR security_id); OPTIONAL - calculate_indicators (true/false) to return only indicator values instead of raw data
|
|
426
|
+
- get_option_chain: REQUIRED - exchange_segment, (symbol OR security_id); OPTIONAL - expiry (YYYY-MM-DD)
|
|
427
|
+
- get_expired_options_data: REQUIRED - exchange_segment, expiry_date (YYYY-MM-DD), (symbol OR security_id)
|
|
428
|
+
|
|
429
|
+
CRITICAL RULES:
|
|
430
|
+
- This is a PLANNING step - after planning, you MUST actually execute the tool calls in your response
|
|
431
|
+
- Do NOT just plan and describe - you MUST actually make the tool calls
|
|
432
|
+
- If the user provides ONLY a symbol (like "RELIANCE", "TCS", "NIFTY") WITHOUT exchange_segment, the workflow MUST be:
|
|
433
|
+
1. First call find_instrument(symbol) to get exchange_segment and security_id - ACTUALLY call it
|
|
434
|
+
2. Then call the data tool (get_live_ltp, get_market_quote, etc.) with the resolved exchange_segment - ACTUALLY call it
|
|
435
|
+
- If the user ALREADY provides exchange_segment (e.g., "on exchange_segment NSE_EQ"), you can call the data tool directly - find_instrument is NOT needed.
|
|
436
|
+
- After planning, your response MUST contain actual tool calls, not descriptions of tool calls
|
|
437
|
+
- For historical data, user MUST provide from_date and to_date in YYYY-MM-DD format
|
|
438
|
+
- For option chain, if user provides expiry date, it must be in YYYY-MM-DD format
|
|
439
|
+
- For expired options data, user MUST provide expiry_date in YYYY-MM-DD format
|
|
440
|
+
|
|
441
|
+
Valid exchange_segment values: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM, IDX_I
|
|
442
|
+
NEVER use "NSE" or "BSE" alone - they are invalid. Use "NSE_EQ" or "BSE_EQ" instead.
|
|
443
|
+
Common indices (NIFTY, BANKNIFTY, FINNIFTY, etc.) use IDX_I, not NSE_EQ.
|
|
444
|
+
|
|
445
|
+
Examples that DO NOT need tools:
|
|
446
|
+
- "Hi", "Hello", "How are you?" → needs_tools: false
|
|
447
|
+
- "What is a stock?" → needs_tools: false (general knowledge)
|
|
448
|
+
- "Explain options trading" → needs_tools: false (educational)
|
|
449
|
+
|
|
450
|
+
If tools are needed, specify which tool(s) and what parameters are required, including date formats (YYYY-MM-DD).
|
|
451
|
+
PROMPT
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def planning_schema
|
|
455
|
+
{
|
|
456
|
+
"type" => "object",
|
|
457
|
+
"required" => ["needs_tools", "reasoning", "tool_requests"],
|
|
458
|
+
"properties" => {
|
|
459
|
+
"needs_tools" => { "type" => "boolean" },
|
|
460
|
+
"reasoning" => { "type" => "string" },
|
|
461
|
+
"tool_requests" => {
|
|
462
|
+
"type" => "array",
|
|
463
|
+
"items" => {
|
|
464
|
+
"type" => "object",
|
|
465
|
+
"required" => ["name", "purpose"],
|
|
466
|
+
"properties" => {
|
|
467
|
+
"name" => { "type" => "string" },
|
|
468
|
+
"purpose" => { "type" => "string" },
|
|
469
|
+
"args_hint" => { "type" => "string" }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def build_tools
|
|
478
|
+
{
|
|
479
|
+
"find_instrument" => lambda do |symbol:|
|
|
480
|
+
DhanHQDataTools.find_instrument(**compact_kwargs(symbol: symbol))
|
|
481
|
+
end,
|
|
482
|
+
"get_market_quote" => lambda do |exchange_segment:, symbol: nil, security_id: nil|
|
|
483
|
+
DhanHQDataTools.get_market_quote(**compact_kwargs(exchange_segment: exchange_segment,
|
|
484
|
+
symbol: symbol,
|
|
485
|
+
security_id: security_id))
|
|
486
|
+
end,
|
|
487
|
+
"get_live_ltp" => lambda do |exchange_segment:, symbol: nil, security_id: nil|
|
|
488
|
+
DhanHQDataTools.get_live_ltp(**compact_kwargs(exchange_segment: exchange_segment,
|
|
489
|
+
symbol: symbol,
|
|
490
|
+
security_id: security_id))
|
|
491
|
+
end,
|
|
492
|
+
"get_market_depth" => lambda do |exchange_segment:, symbol: nil, security_id: nil|
|
|
493
|
+
DhanHQDataTools.get_market_depth(**compact_kwargs(exchange_segment: exchange_segment,
|
|
494
|
+
symbol: symbol,
|
|
495
|
+
security_id: security_id))
|
|
496
|
+
end,
|
|
497
|
+
"get_historical_data" => lambda do |exchange_segment:, from_date:, to_date:, symbol: nil, security_id: nil,
|
|
498
|
+
interval: nil, expiry_code: nil, calculate_indicators: false|
|
|
499
|
+
# Convert security_id to integer if provided (LLM may pass it as string)
|
|
500
|
+
normalized_security_id = security_id&.to_i
|
|
501
|
+
DhanHQDataTools.get_historical_data(**compact_kwargs(exchange_segment: exchange_segment,
|
|
502
|
+
symbol: symbol,
|
|
503
|
+
security_id: normalized_security_id,
|
|
504
|
+
from_date: from_date,
|
|
505
|
+
to_date: to_date,
|
|
506
|
+
interval: interval,
|
|
507
|
+
expiry_code: expiry_code,
|
|
508
|
+
calculate_indicators: calculate_indicators))
|
|
509
|
+
end,
|
|
510
|
+
"get_expiry_list" => lambda do |exchange_segment:, symbol: nil, security_id: nil|
|
|
511
|
+
# Convert security_id to integer if provided (LLM may pass it as string)
|
|
512
|
+
normalized_security_id = security_id&.to_i
|
|
513
|
+
DhanHQDataTools.get_expiry_list(**compact_kwargs(exchange_segment: exchange_segment,
|
|
514
|
+
symbol: symbol,
|
|
515
|
+
security_id: normalized_security_id))
|
|
516
|
+
end,
|
|
517
|
+
"get_option_chain" => lambda do |exchange_segment:, symbol: nil, security_id: nil, expiry: nil, strikes_count: 5|
|
|
518
|
+
# Convert security_id to integer if provided (LLM may pass it as string)
|
|
519
|
+
normalized_security_id = security_id&.to_i
|
|
520
|
+
# Default to 5 strikes (2 ITM, ATM, 2 OTM) - good balance for analysis
|
|
521
|
+
normalized_strikes_count = strikes_count.to_i
|
|
522
|
+
normalized_strikes_count = 5 if normalized_strikes_count < 1 # Minimum 1 (ATM)
|
|
523
|
+
DhanHQDataTools.get_option_chain(**compact_kwargs(exchange_segment: exchange_segment,
|
|
524
|
+
symbol: symbol,
|
|
525
|
+
security_id: normalized_security_id,
|
|
526
|
+
expiry: expiry,
|
|
527
|
+
strikes_count: normalized_strikes_count))
|
|
528
|
+
end,
|
|
529
|
+
"get_expired_options_data" => lambda do |exchange_segment:, expiry_date:, symbol: nil, security_id: nil,
|
|
530
|
+
interval: nil, instrument: nil, expiry_flag: nil, expiry_code: nil,
|
|
531
|
+
strike: nil, drv_option_type: nil, required_data: nil|
|
|
532
|
+
DhanHQDataTools.get_expired_options_data(
|
|
533
|
+
**compact_kwargs(exchange_segment: exchange_segment,
|
|
534
|
+
expiry_date: expiry_date,
|
|
535
|
+
symbol: symbol,
|
|
536
|
+
security_id: security_id,
|
|
537
|
+
interval: interval,
|
|
538
|
+
instrument: instrument,
|
|
539
|
+
expiry_flag: expiry_flag,
|
|
540
|
+
expiry_code: expiry_code,
|
|
541
|
+
strike: strike,
|
|
542
|
+
drv_option_type: drv_option_type,
|
|
543
|
+
required_data: required_data)
|
|
544
|
+
)
|
|
545
|
+
end
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def compact_kwargs(kwargs)
|
|
550
|
+
kwargs.reject { |_, value| value.nil? || value == "" }
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def tool_messages(messages)
|
|
554
|
+
messages.select { |message| message[:role] == "tool" }
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def print_tool_results(messages)
|
|
558
|
+
tool_messages(messages).each do |message|
|
|
559
|
+
print_tool_message(message)
|
|
560
|
+
end
|
|
561
|
+
puts # blank line after tool results
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def print_tool_message(message)
|
|
565
|
+
tool_name = message[:name] || "unknown_tool"
|
|
566
|
+
content = parse_tool_content(message[:content])
|
|
567
|
+
|
|
568
|
+
puts "\n#{COLOR_LLM}🔧 Tool Called:#{COLOR_RESET} #{tool_name}"
|
|
569
|
+
print_formatted_result(tool_name, content)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def format_tool_content(content)
|
|
573
|
+
parsed = parse_tool_content(content)
|
|
574
|
+
return parsed if parsed.is_a?(String)
|
|
575
|
+
|
|
576
|
+
JSON.pretty_generate(parsed)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def parse_tool_content(content)
|
|
580
|
+
return content unless content.is_a?(String)
|
|
581
|
+
|
|
582
|
+
JSON.parse(content)
|
|
583
|
+
rescue JSON::ParserError
|
|
584
|
+
content
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def print_formatted_result(tool_name, content)
|
|
588
|
+
return puts content if content.is_a?(String)
|
|
589
|
+
|
|
590
|
+
result = content["result"] || content
|
|
591
|
+
|
|
592
|
+
case tool_name
|
|
593
|
+
when "get_live_ltp"
|
|
594
|
+
print_ltp_result(result)
|
|
595
|
+
when "get_market_quote"
|
|
596
|
+
print_quote_result(result)
|
|
597
|
+
when "get_historical_data"
|
|
598
|
+
print_historical_result(result, content)
|
|
599
|
+
when "get_option_chain"
|
|
600
|
+
print_option_chain_result(result)
|
|
601
|
+
when "get_expiry_list"
|
|
602
|
+
print_expiry_list_result(result)
|
|
603
|
+
when "find_instrument"
|
|
604
|
+
print_instrument_result(result)
|
|
605
|
+
else
|
|
606
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} #{JSON.pretty_generate(content)}"
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def print_ltp_result(result)
|
|
611
|
+
symbol = result["symbol"] || "Unknown"
|
|
612
|
+
ltp = result["ltp"] || result.dig("ltp_data", "last_price")
|
|
613
|
+
exchange = result["exchange_segment"]
|
|
614
|
+
|
|
615
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} #{symbol} (#{exchange})"
|
|
616
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Last Price: ₹#{ltp}"
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def print_quote_result(result)
|
|
620
|
+
symbol = result["symbol"] || "Unknown"
|
|
621
|
+
quote = result["quote"] || {}
|
|
622
|
+
ohlc = quote["ohlc"] || {}
|
|
623
|
+
|
|
624
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} #{symbol}"
|
|
625
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} LTP: ₹#{quote['last_price']}"
|
|
626
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} OHLC: O:#{ohlc['open']} H:#{ohlc['high']} L:#{ohlc['low']} C:#{ohlc['close']}"
|
|
627
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Volume: #{quote['volume']}" if quote["volume"]&.positive?
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def print_historical_result(result, content)
|
|
631
|
+
if result.is_a?(Hash) && result.key?("indicators")
|
|
632
|
+
print_indicator_result(result)
|
|
633
|
+
elsif result.is_a?(Hash)
|
|
634
|
+
data_points = result["data"]&.size || 0
|
|
635
|
+
interval = content.dig("params", "interval")
|
|
636
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Historical data: #{data_points} records"
|
|
637
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Interval: #{interval}" if interval
|
|
638
|
+
elsif result.is_a?(Array)
|
|
639
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} #{result.size} data points"
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def print_indicator_result(result)
|
|
644
|
+
indicators = result["indicators"]
|
|
645
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Technical Indicators:"
|
|
646
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Current Price: ₹#{indicators['current_price']}"
|
|
647
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} RSI(14): #{indicators['rsi']&.round(2)}"
|
|
648
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} MACD: #{indicators.dig('macd', 'macd')&.round(2)}"
|
|
649
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} SMA(20): ₹#{indicators['sma_20']&.round(2)}"
|
|
650
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} SMA(50): ₹#{indicators['sma_50']&.round(2)}"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def print_option_chain_result(result)
|
|
654
|
+
oc = result.dig("data", "oc") || {}
|
|
655
|
+
last_price = result.dig("data", "last_price")
|
|
656
|
+
strikes = oc.keys.size
|
|
657
|
+
|
|
658
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Spot: ₹#{last_price}"
|
|
659
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Strikes: #{strikes}"
|
|
660
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} (Filtered: ATM/OTM/ITM with both CE & PE)"
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def print_expiry_list_result(result)
|
|
664
|
+
expiries = result["expiries"] || []
|
|
665
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Available expiries: #{expiries.size}"
|
|
666
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Next expiry: #{expiries.first}" if expiries.any?
|
|
667
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Expiries: #{expiries[0..4].join(', ')}#{'...' if expiries.size > 5}"
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def print_instrument_result(result)
|
|
671
|
+
symbol = result["symbol"]
|
|
672
|
+
security_id = result["security_id"]
|
|
673
|
+
exchange = result["exchange_segment"]
|
|
674
|
+
|
|
675
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Found: #{symbol}"
|
|
676
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Security ID: #{security_id}"
|
|
677
|
+
puts " #{COLOR_LLM}→#{COLOR_RESET} Exchange: #{exchange}"
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def show_llm_summary?
|
|
681
|
+
ENV["SHOW_LLM_SUMMARY"] == "true"
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def allow_no_tool_output?
|
|
685
|
+
ENV["ALLOW_NO_TOOL_OUTPUT"] != "false"
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def show_plan?
|
|
689
|
+
ENV["SHOW_PLAN"] == "true"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def print_hallucination_warning
|
|
693
|
+
puts "No tool results were produced."
|
|
694
|
+
puts "LLM output suppressed to avoid hallucinated data."
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def query_requires_tools?(query)
|
|
698
|
+
tool_keywords = [
|
|
699
|
+
"get_market_quote", "market quote", "quote",
|
|
700
|
+
"get_live_ltp", "ltp", "last traded price", "current price",
|
|
701
|
+
"get_market_depth", "market depth", "depth",
|
|
702
|
+
"get_historical_data", "historical", "price history", "candles", "ohlc",
|
|
703
|
+
"get_option_chain", "option chain", "options chain",
|
|
704
|
+
"get_expired_options", "expired options"
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
query_lower = query.downcase
|
|
708
|
+
tool_keywords.any? { |keyword| query_lower.include?(keyword) }
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def plan_for_query(client, query, config)
|
|
712
|
+
response = client.chat_raw(
|
|
713
|
+
messages: [
|
|
714
|
+
{ role: "system", content: planning_system_prompt },
|
|
715
|
+
{ role: "user", content: query }
|
|
716
|
+
],
|
|
717
|
+
allow_chat: true,
|
|
718
|
+
format: planning_schema,
|
|
719
|
+
options: { temperature: config.temperature }
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
plan = parse_tool_content(response.message&.content.to_s)
|
|
723
|
+
|
|
724
|
+
if !plan["needs_tools"] && query_requires_tools?(query)
|
|
725
|
+
plan["needs_tools"] = true
|
|
726
|
+
plan["reasoning"] = "Query contains tool-related keywords that require data retrieval."
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
plan
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def print_plan(plan)
|
|
733
|
+
return unless show_plan?
|
|
734
|
+
|
|
735
|
+
puts "Plan:"
|
|
736
|
+
puts "- Needs tools: #{plan['needs_tools']}"
|
|
737
|
+
puts "- Reasoning: #{plan['reasoning']}"
|
|
738
|
+
return if plan["tool_requests"].empty?
|
|
739
|
+
|
|
740
|
+
puts "- Tool requests:"
|
|
741
|
+
plan["tool_requests"].each do |request|
|
|
742
|
+
args_hint = request["args_hint"]
|
|
743
|
+
suffix = args_hint && !args_hint.empty? ? " (#{args_hint})" : ""
|
|
744
|
+
puts " - #{request['name']}: #{request['purpose']}#{suffix}"
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def chat_response(client, messages, config)
|
|
749
|
+
content = +""
|
|
750
|
+
print LLM_PROMPT
|
|
751
|
+
|
|
752
|
+
client.chat_raw(
|
|
753
|
+
messages: messages,
|
|
754
|
+
allow_chat: true,
|
|
755
|
+
options: { temperature: config.temperature },
|
|
756
|
+
stream: true
|
|
757
|
+
) do |chunk|
|
|
758
|
+
token = chunk.dig("message", "content").to_s
|
|
759
|
+
next if token.empty?
|
|
760
|
+
|
|
761
|
+
content << token
|
|
762
|
+
print token
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
puts
|
|
766
|
+
content
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
class ConsoleStream
|
|
770
|
+
def initialize
|
|
771
|
+
@started = false
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def emit(event, text: nil, **)
|
|
775
|
+
return unless event == :token && text
|
|
776
|
+
|
|
777
|
+
unless @started
|
|
778
|
+
print LLM_PROMPT
|
|
779
|
+
@started = true
|
|
780
|
+
end
|
|
781
|
+
print text
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def finish
|
|
785
|
+
puts if @started
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def run_console(client, config)
|
|
790
|
+
configure_dhanhq!
|
|
791
|
+
print_banner(config)
|
|
792
|
+
reader = build_reader
|
|
793
|
+
load_history(reader, HISTORY_PATH)
|
|
794
|
+
tools = build_tools
|
|
795
|
+
system_prompt = [tool_system_prompt, system_prompt_from_env].compact.join("\n\n")
|
|
796
|
+
chat_messages = []
|
|
797
|
+
system_prompt_from_env&.then { |prompt| chat_messages << { role: "system", content: prompt } }
|
|
798
|
+
|
|
799
|
+
loop do
|
|
800
|
+
input = read_input(reader)
|
|
801
|
+
break unless input
|
|
802
|
+
|
|
803
|
+
text = input.strip
|
|
804
|
+
next if text.empty?
|
|
805
|
+
break if exit_command?(text)
|
|
806
|
+
|
|
807
|
+
update_history(HISTORY_PATH, text)
|
|
808
|
+
plan = plan_for_query(client, text, config)
|
|
809
|
+
print_plan(plan)
|
|
810
|
+
|
|
811
|
+
unless plan["needs_tools"]
|
|
812
|
+
chat_messages << { role: "user", content: text }
|
|
813
|
+
content = chat_response(client, chat_messages, config)
|
|
814
|
+
chat_messages << { role: "assistant", content: content }
|
|
815
|
+
next
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
stream = show_llm_summary? ? ConsoleStream.new : nil
|
|
819
|
+
executor = Ollama::Agent::Executor.new(client, tools: tools, max_steps: 10, stream: stream)
|
|
820
|
+
result = executor.run(system: system_prompt, user: text)
|
|
821
|
+
stream&.finish
|
|
822
|
+
|
|
823
|
+
if tool_messages(executor.messages).empty?
|
|
824
|
+
if allow_no_tool_output?
|
|
825
|
+
puts "No tool results were produced."
|
|
826
|
+
print LLM_PROMPT
|
|
827
|
+
puts result
|
|
828
|
+
else
|
|
829
|
+
print_hallucination_warning
|
|
830
|
+
end
|
|
831
|
+
else
|
|
832
|
+
print_tool_results(executor.messages)
|
|
833
|
+
print LLM_PROMPT
|
|
834
|
+
puts result
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
rescue Interrupt
|
|
838
|
+
puts "\nExiting..."
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
config = build_config
|
|
842
|
+
client = Ollama::Client.new(config: config)
|
|
843
|
+
run_console(client, config)
|