DhanHQ 3.2.0 → 3.2.1
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/AGENTS.md +23 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +53 -34
- data/lib/DhanHQ/models/alert_order.rb +1 -1
- data/lib/DhanHQ/risk/pipeline.rb +0 -2
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/dhan_hq.rb +4 -0
- data/skills/dhanhq-ruby/SKILL.md +208 -0
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
- data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
- data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
- data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
- data/skills/dhanhq-ruby/references/error-codes.md +50 -0
- data/skills/dhanhq-ruby/references/funds.md +67 -0
- data/skills/dhanhq-ruby/references/instruments.md +91 -0
- data/skills/dhanhq-ruby/references/live-feed.md +83 -0
- data/skills/dhanhq-ruby/references/market-data.md +119 -0
- data/skills/dhanhq-ruby/references/option-chain.md +71 -0
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
- data/skills/dhanhq-ruby/references/orders.md +203 -0
- data/skills/dhanhq-ruby/references/portfolio.md +93 -0
- data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
- data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
- metadata +32 -2
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dhan_hq"
|
|
4
|
+
require "json"
|
|
5
|
+
require "csv"
|
|
6
|
+
|
|
7
|
+
# Resolution helper for credentials and loading config files.
|
|
8
|
+
def _load_config(path)
|
|
9
|
+
JSON.parse(File.read(path))
|
|
10
|
+
rescue StandardError
|
|
11
|
+
{}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Initialize the DhanHQ configuration.
|
|
15
|
+
def get_client(config_path = nil)
|
|
16
|
+
client_id = nil
|
|
17
|
+
access_token = nil
|
|
18
|
+
|
|
19
|
+
paths_to_try = [
|
|
20
|
+
config_path,
|
|
21
|
+
ENV.fetch("DHAN_CONFIG_PATH", nil),
|
|
22
|
+
"config.json"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
paths_to_try.each do |path|
|
|
26
|
+
next if path.nil? || path.to_s.empty? || !File.exist?(path)
|
|
27
|
+
|
|
28
|
+
config = _load_config(path)
|
|
29
|
+
client_id = config["client_id"] || config[:client_id] || client_id
|
|
30
|
+
access_token = config["access_token"] || config[:access_token] || access_token
|
|
31
|
+
break if client_id && access_token
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
client_id ||= ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
35
|
+
access_token ||= ENV.fetch("DHAN_ACCESS_TOKEN", nil)
|
|
36
|
+
|
|
37
|
+
if client_id.nil? || client_id.to_s.empty? || access_token.nil? || access_token.to_s.empty?
|
|
38
|
+
raise ArgumentError, "Credentials not found. Set DHAN_CLIENT_ID and DHAN_ACCESS_TOKEN, or use config.json"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
DhanHQ.configure do |config|
|
|
42
|
+
config.client_id = client_id.to_s
|
|
43
|
+
config.access_token = access_token.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
DhanHQ.ensure_configuration!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extract data field or raise error if response failed.
|
|
50
|
+
def unwrap_sdk_data(response)
|
|
51
|
+
if response.respond_to?(:key?)
|
|
52
|
+
status = response[:status] || response["status"]
|
|
53
|
+
remarks = response[:remarks] || response["remarks"]
|
|
54
|
+
data = response[:data] || response["data"]
|
|
55
|
+
|
|
56
|
+
raise remarks.to_s unless status == "success"
|
|
57
|
+
|
|
58
|
+
return data
|
|
59
|
+
end
|
|
60
|
+
response
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Resolve a symbol to its security details.
|
|
64
|
+
def resolve_symbol(symbol, exchange_segment = DhanHQ::Constants::ExchangeSegment::NSE_EQ, _instrument_name = DhanHQ::Constants::InstrumentType::EQUITY)
|
|
65
|
+
inst = DhanHQ::Models::Instrument.find(exchange_segment, symbol, exact_match: true)
|
|
66
|
+
inst ||= DhanHQ::Models::Instrument.find(exchange_segment, symbol, exact_match: false)
|
|
67
|
+
return nil unless inst
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
"security_id" => inst.security_id.to_s,
|
|
71
|
+
"trading_symbol" => inst.symbol_name,
|
|
72
|
+
"display_name" => inst.display_name,
|
|
73
|
+
"exchange_segment" => inst.exchange_segment,
|
|
74
|
+
"instrument_name" => inst.instrument
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolve derivative details like Strike/Expiry/OptionType from the security list.
|
|
79
|
+
def resolve_derivative(underlying, strike: nil, option_type: nil, expiry: nil, exchange: "NSE")
|
|
80
|
+
exchange_segment = case exchange.to_s.upcase
|
|
81
|
+
when "NSE" then DhanHQ::Constants::ExchangeSegment::NSE_FNO
|
|
82
|
+
when "BSE" then DhanHQ::Constants::ExchangeSegment::BSE_FNO
|
|
83
|
+
when "MCX" then DhanHQ::Constants::ExchangeSegment::MCX_COMM
|
|
84
|
+
else DhanHQ::Constants::ExchangeSegment::NSE_FNO
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
instruments = DhanHQ::Models::Instrument.by_segment(exchange_segment)
|
|
88
|
+
return nil if instruments.empty?
|
|
89
|
+
|
|
90
|
+
matches = instruments.select do |inst|
|
|
91
|
+
symbol_match = inst.underlying_symbol&.upcase == underlying.upcase ||
|
|
92
|
+
inst.symbol_name&.upcase&.start_with?(underlying.upcase)
|
|
93
|
+
|
|
94
|
+
match = symbol_match
|
|
95
|
+
|
|
96
|
+
match &&= inst.strike_price&.to_f == strike.to_f if strike
|
|
97
|
+
|
|
98
|
+
if option_type
|
|
99
|
+
match &&= if option_type.to_s.upcase == "FUT"
|
|
100
|
+
inst.instrument&.upcase&.include?("FUT")
|
|
101
|
+
else
|
|
102
|
+
inst.option_type&.upcase == option_type.to_s.upcase
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
match &&= inst.expiry_date == expiry if expiry
|
|
107
|
+
|
|
108
|
+
match
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
matches = matches.sort_by { |inst| [inst.expiry_date.to_s, inst.symbol_name.to_s] }
|
|
112
|
+
return nil if matches.empty?
|
|
113
|
+
|
|
114
|
+
row = matches.first
|
|
115
|
+
{
|
|
116
|
+
"security_id" => row.security_id.to_s,
|
|
117
|
+
"trading_symbol" => row.symbol_name,
|
|
118
|
+
"lot_size" => row.lot_size&.to_i || 1,
|
|
119
|
+
"tick_size" => row.tick_size&.to_f || 0.05,
|
|
120
|
+
"expiry" => row.expiry_date,
|
|
121
|
+
"instrument_name" => row.instrument
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Fetch lot size of an instrument from common fallback table or security master.
|
|
126
|
+
def get_lot_size(security_id: nil, trading_symbol: nil, underlying: nil)
|
|
127
|
+
common_lots = { "NIFTY" => 75, "BANKNIFTY" => 15, "FINNIFTY" => 25, "MIDCPNIFTY" => 50, "SENSEX" => 10 }
|
|
128
|
+
|
|
129
|
+
if underlying
|
|
130
|
+
name = underlying.upcase.gsub(/\s+/, "")
|
|
131
|
+
return common_lots[name] if common_lots.key?(name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if trading_symbol
|
|
135
|
+
name = trading_symbol.upcase
|
|
136
|
+
common_lots.each { |k, v| return v if name.include?(k) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Lookup instrument via segment
|
|
140
|
+
if security_id
|
|
141
|
+
%w[NSE_EQ NSE_FNO IDX_I].each do |seg|
|
|
142
|
+
insts = begin
|
|
143
|
+
DhanHQ::Models::Instrument.by_segment(seg)
|
|
144
|
+
rescue StandardError
|
|
145
|
+
[]
|
|
146
|
+
end
|
|
147
|
+
inst = insts.find { |i| i.security_id.to_s == security_id.to_s }
|
|
148
|
+
return inst.lot_size.to_i if inst&.lot_size
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if trading_symbol
|
|
153
|
+
inst = DhanHQ::Models::Instrument.find_anywhere(trading_symbol)
|
|
154
|
+
return inst.lot_size.to_i if inst&.lot_size
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Build a human-readable order preview.
|
|
161
|
+
def preview_order(security_id:, exchange_segment:, transaction_type:, quantity:, order_type:, product_type:, price: 0.0, trading_symbol: nil)
|
|
162
|
+
notional = price.to_f * quantity.to_i
|
|
163
|
+
lines = [
|
|
164
|
+
"--- ORDER PREVIEW ---",
|
|
165
|
+
"Security: #{trading_symbol || security_id}",
|
|
166
|
+
"Exchange: #{exchange_segment}",
|
|
167
|
+
"Action: #{transaction_type}",
|
|
168
|
+
"Quantity: #{quantity}",
|
|
169
|
+
"Order Type: #{order_type}",
|
|
170
|
+
"Product Type: #{product_type}",
|
|
171
|
+
"Price: #{order_type.to_s.upcase == DhanHQ::Constants::OrderType::MARKET ? "MARKET / MPP" : "Rs. #{"%.2f" % price}"}"
|
|
172
|
+
]
|
|
173
|
+
if notional.positive? && order_type.to_s.upcase != DhanHQ::Constants::OrderType::MARKET
|
|
174
|
+
lines << "Notional: Rs. #{"%.2f" % notional}"
|
|
175
|
+
lines << "Warning: Notional exceeds Rs. 50,000" if notional > 50_000
|
|
176
|
+
end
|
|
177
|
+
lines << "---------------------"
|
|
178
|
+
lines.join("\n")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Normalize option-chain data format.
|
|
182
|
+
def normalize_option_chain(response)
|
|
183
|
+
data = response[:data] || response["data"] || response
|
|
184
|
+
|
|
185
|
+
if data.key?(:strikes) || data.key?("strikes")
|
|
186
|
+
spot = data[:last_price] || data["last_price"]
|
|
187
|
+
strikes = data[:strikes] || data["strikes"]
|
|
188
|
+
|
|
189
|
+
rows = strikes.map do |s|
|
|
190
|
+
strike = s[:strike] || s["strike"]
|
|
191
|
+
ce = s[:call] || s["call"] || {}
|
|
192
|
+
pe = s[:put] || s["put"] || {}
|
|
193
|
+
|
|
194
|
+
row = { "strike" => strike.to_f }
|
|
195
|
+
|
|
196
|
+
# ce fields
|
|
197
|
+
row["ce_security_id"] = ce["security_id"] || ce[:security_id]
|
|
198
|
+
row["ce_ltp"] = ce["last_price"] || ce[:last_price]
|
|
199
|
+
row["ce_avg_price"] = ce["average_price"] || ce[:average_price]
|
|
200
|
+
row["ce_oi"] = ce["oi"] || ce[:oi]
|
|
201
|
+
row["ce_oi_change"] = ce["oi_change"] || ce[:oi_change]
|
|
202
|
+
row["ce_volume"] = ce["volume"] || ce[:volume]
|
|
203
|
+
row["ce_iv"] = ce["implied_volatility"] || ce[:implied_volatility]
|
|
204
|
+
row["ce_bid_price"] = ce["top_bid_price"] || ce[:top_bid_price]
|
|
205
|
+
row["ce_bid_qty"] = ce["top_bid_quantity"] || ce[:top_bid_quantity]
|
|
206
|
+
row["ce_ask_price"] = ce["top_ask_price"] || ce[:top_ask_price]
|
|
207
|
+
row["ce_ask_qty"] = ce["top_ask_quantity"] || ce[:top_ask_quantity]
|
|
208
|
+
|
|
209
|
+
ce_greeks = ce["greeks"] || ce[:greeks] || {}
|
|
210
|
+
row["ce_delta"] = ce_greeks["delta"] || ce_greeks[:delta]
|
|
211
|
+
row["ce_gamma"] = ce_greeks["gamma"] || ce_greeks[:gamma]
|
|
212
|
+
row["ce_theta"] = ce_greeks["theta"] || ce_greeks[:theta]
|
|
213
|
+
row["ce_vega"] = ce_greeks["vega"] || ce_greeks[:vega]
|
|
214
|
+
|
|
215
|
+
# pe fields
|
|
216
|
+
row["pe_security_id"] = pe["security_id"] || pe[:security_id]
|
|
217
|
+
row["pe_ltp"] = pe["last_price"] || pe[:last_price]
|
|
218
|
+
row["pe_avg_price"] = pe["average_price"] || pe[:average_price]
|
|
219
|
+
row["pe_oi"] = pe["oi"] || pe[:oi]
|
|
220
|
+
row["pe_oi_change"] = pe["oi_change"] || pe[:oi_change]
|
|
221
|
+
row["pe_volume"] = pe["volume"] || pe[:volume]
|
|
222
|
+
row["pe_iv"] = pe["implied_volatility"] || pe[:implied_volatility]
|
|
223
|
+
row["pe_bid_price"] = pe["top_bid_price"] || pe[:top_bid_price]
|
|
224
|
+
row["pe_bid_qty"] = pe["top_bid_quantity"] || pe[:top_bid_quantity]
|
|
225
|
+
row["pe_ask_price"] = pe["top_ask_price"] || pe[:top_ask_price]
|
|
226
|
+
row["pe_ask_qty"] = pe["top_ask_quantity"] || pe[:top_ask_quantity]
|
|
227
|
+
|
|
228
|
+
pe_greeks = pe["greeks"] || pe[:greeks] || {}
|
|
229
|
+
row["pe_delta"] = pe_greeks["delta"] || pe_greeks[:delta]
|
|
230
|
+
row["pe_gamma"] = pe_greeks["gamma"] || pe_greeks[:gamma]
|
|
231
|
+
row["pe_theta"] = pe_greeks["theta"] || pe_greeks[:theta]
|
|
232
|
+
row["pe_vega"] = pe_greeks["vega"] || pe_greeks[:vega]
|
|
233
|
+
|
|
234
|
+
row
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
return spot.to_f, rows
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
[0.0, []]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Fetch option-chain and return the rows.
|
|
244
|
+
def fetch_chain_df(_dhan_client = nil, under_security_id:, expiry:, under_exchange_segment: DhanHQ::Constants::ExchangeSegment::IDX_I)
|
|
245
|
+
chain = DhanHQ::Models::OptionChain.fetch(
|
|
246
|
+
underlying_scrip: under_security_id.to_i,
|
|
247
|
+
underlying_seg: under_exchange_segment,
|
|
248
|
+
expiry: expiry
|
|
249
|
+
)
|
|
250
|
+
normalize_option_chain(chain)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Find ATM row nearest to spot.
|
|
254
|
+
def find_atm_row(chain_rows, spot)
|
|
255
|
+
chain_rows.min_by { |row| (row["strike"].to_f - spot.to_f).abs }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Aggregate holdings and positions into summary.
|
|
259
|
+
def format_pnl_report(holdings = nil, positions = nil)
|
|
260
|
+
holdings_list = holdings || begin
|
|
261
|
+
DhanHQ::Models::Holding.all
|
|
262
|
+
rescue StandardError
|
|
263
|
+
[]
|
|
264
|
+
end
|
|
265
|
+
positions_list = positions || begin
|
|
266
|
+
DhanHQ::Models::Position.all
|
|
267
|
+
rescue StandardError
|
|
268
|
+
[]
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
report = {
|
|
272
|
+
"total_investment" => 0.0,
|
|
273
|
+
"current_value" => 0.0,
|
|
274
|
+
"total_pnl" => 0.0,
|
|
275
|
+
"day_pnl" => 0.0,
|
|
276
|
+
"holdings_count" => holdings_list.size,
|
|
277
|
+
"positions_count" => positions_list.size
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
holdings_list.each do |holding|
|
|
281
|
+
qty = holding.respond_to?(:total_qty) ? holding.total_qty : (holding[:totalQty] || holding["totalQty"] || 0)
|
|
282
|
+
cost = holding.respond_to?(:avg_cost_price) ? holding.avg_cost_price : (holding[:avgCostPrice] || holding["avgCostPrice"] || 0)
|
|
283
|
+
val = holding.respond_to?(:market_value) ? holding.market_value : (holding[:marketValue] || holding["marketValue"] || 0)
|
|
284
|
+
pnl = holding.respond_to?(:pnl) ? holding.pnl : (holding[:pnl] || holding["pnl"] || 0)
|
|
285
|
+
day_pnl = holding.respond_to?(:day_pnl) ? holding.day_pnl : (holding[:dayPnl] || holding["dayPnl"] || 0)
|
|
286
|
+
|
|
287
|
+
report["total_investment"] += cost.to_f * qty.to_f
|
|
288
|
+
report["current_value"] += val.to_f
|
|
289
|
+
report["total_pnl"] += pnl.to_f
|
|
290
|
+
report["day_pnl"] += day_pnl.to_f
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
positions_list.each do |position|
|
|
294
|
+
realized = position.respond_to?(:realized_profit) ? position.realized_profit : (position[:realizedProfit] || position["realizedProfit"] || 0)
|
|
295
|
+
unrealized = position.respond_to?(:unrealized_profit) ? position.unrealized_profit : (position[:unrealizedProfit] || position["unrealizedProfit"] || 0)
|
|
296
|
+
report["total_pnl"] += realized.to_f + unrealized.to_f
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
report
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Run pre-flight margin checks against Dhan API.
|
|
303
|
+
def check_margin(_dhan_client = nil, security_id:, exchange_segment:, transaction_type:, quantity:, product_type:, price:, trigger_price: 0.0)
|
|
304
|
+
margin = DhanHQ::Models::Margin.calculate(
|
|
305
|
+
security_id: security_id.to_s,
|
|
306
|
+
exchange_segment: exchange_segment,
|
|
307
|
+
transaction_type: transaction_type,
|
|
308
|
+
quantity: quantity,
|
|
309
|
+
product_type: product_type,
|
|
310
|
+
price: price,
|
|
311
|
+
trigger_price: trigger_price
|
|
312
|
+
)
|
|
313
|
+
DhanHQ::Models::Funds.fetch
|
|
314
|
+
|
|
315
|
+
{
|
|
316
|
+
"total_margin" => margin.total_margin.to_f,
|
|
317
|
+
"available_balance" => margin.available_balance.to_f,
|
|
318
|
+
"brokerage" => margin.brokerage.to_f,
|
|
319
|
+
"leverage" => margin.leverage.to_f,
|
|
320
|
+
"sufficient" => margin.available_balance.to_f >= margin.total_margin.to_f,
|
|
321
|
+
"shortfall" => [0.0, margin.total_margin.to_f - margin.available_balance.to_f].max
|
|
322
|
+
}
|
|
323
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rubocop:disable Style/FormatStringToken
|
|
3
|
+
# rubocop:disable Performance/CollectionLiteralInLoop
|
|
4
|
+
|
|
5
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
6
|
+
require "dhan_hq"
|
|
7
|
+
require_relative "dhan_helpers"
|
|
8
|
+
|
|
9
|
+
def load_security_master(_segment = "compact")
|
|
10
|
+
puts "Loading security master segments..."
|
|
11
|
+
# Unlike Python, Ruby SDK by_segment is segment-specific.
|
|
12
|
+
# We will resolve dynamically when searching.
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def search_equity(query, limit = 10)
|
|
16
|
+
query_upper = query.to_s.upcase.strip
|
|
17
|
+
|
|
18
|
+
results = []
|
|
19
|
+
%w[NSE_EQ BSE_EQ].each do |segment|
|
|
20
|
+
instruments = begin
|
|
21
|
+
DhanHQ::Models::Instrument.by_segment(segment)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
instruments.each do |inst|
|
|
26
|
+
next unless inst.instrument == DhanHQ::Constants::InstrumentType::EQUITY
|
|
27
|
+
|
|
28
|
+
symbol_name = inst.symbol_name.to_s.upcase
|
|
29
|
+
underlying = inst.underlying_symbol.to_s.upcase
|
|
30
|
+
display_name = inst.display_name.to_s.upcase
|
|
31
|
+
|
|
32
|
+
if symbol_name == query_upper || display_name == query_upper
|
|
33
|
+
results << inst
|
|
34
|
+
elsif symbol_name.include?(query_upper) || display_name.include?(query_upper) || underlying.include?(query_upper)
|
|
35
|
+
results << inst
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
break if results.size >= limit
|
|
39
|
+
end
|
|
40
|
+
break if results.size >= limit
|
|
41
|
+
end
|
|
42
|
+
begin
|
|
43
|
+
results.head(limit)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
results.first(limit)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def search_derivative(underlying, strike: nil, option_type: nil, expiry: nil, limit: 20)
|
|
50
|
+
# Look in NSE_FNO and BSE_FNO
|
|
51
|
+
results = []
|
|
52
|
+
%w[NSE_FNO BSE_FNO].each do |segment|
|
|
53
|
+
instruments = begin
|
|
54
|
+
DhanHQ::Models::Instrument.by_segment(segment)
|
|
55
|
+
rescue StandardError
|
|
56
|
+
[]
|
|
57
|
+
end
|
|
58
|
+
matched = instruments.select do |inst|
|
|
59
|
+
symbol_match = inst.underlying_symbol&.upcase == underlying.upcase ||
|
|
60
|
+
inst.symbol_name&.upcase&.start_with?(underlying.upcase)
|
|
61
|
+
|
|
62
|
+
match = symbol_match
|
|
63
|
+
|
|
64
|
+
if option_type == "FUT"
|
|
65
|
+
match &&= inst.instrument&.upcase&.include?("FUT")
|
|
66
|
+
elsif %w[CE PE].include?(option_type)
|
|
67
|
+
match &&= inst.option_type&.upcase == option_type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
match &&= inst.strike_price&.to_f == strike.to_f if strike
|
|
71
|
+
|
|
72
|
+
match &&= inst.expiry_date == expiry if expiry
|
|
73
|
+
|
|
74
|
+
match
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
results.concat(matched)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
results.sort_by { |inst| [inst.expiry_date.to_s, inst.symbol_name.to_s] }.first(limit)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_query(query)
|
|
84
|
+
parts = query.upcase.split
|
|
85
|
+
|
|
86
|
+
return { type: :equity, name: query } if parts.size <= 2 && parts.none? { |p| %w[CE PE FUT FUTURE].include?(p) }
|
|
87
|
+
|
|
88
|
+
underlying = parts[0]
|
|
89
|
+
strike = nil
|
|
90
|
+
option_type = nil
|
|
91
|
+
expiry = nil
|
|
92
|
+
|
|
93
|
+
parts[1..].each do |part|
|
|
94
|
+
if %w[CE PE].include?(part)
|
|
95
|
+
option_type = part
|
|
96
|
+
elsif %w[FUT FUTURE].include?(part)
|
|
97
|
+
option_type = "FUT"
|
|
98
|
+
elsif part.include?("-") && part.length == 10
|
|
99
|
+
expiry = part
|
|
100
|
+
else
|
|
101
|
+
begin
|
|
102
|
+
strike = Float(part)
|
|
103
|
+
rescue ArgumentError
|
|
104
|
+
underlying += " #{part}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
type: :fno,
|
|
111
|
+
underlying: underlying,
|
|
112
|
+
strike: strike,
|
|
113
|
+
option_type: option_type,
|
|
114
|
+
expiry: expiry
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def main
|
|
119
|
+
if ARGV.empty?
|
|
120
|
+
puts "Usage: ruby scripts/resolve_security.rb <query>"
|
|
121
|
+
puts "Examples: \"RELIANCE\", \"HDFC Bank\", \"NIFTY 24000 CE 2025-03-27\""
|
|
122
|
+
exit 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
query = ARGV.join(" ")
|
|
126
|
+
parsed = parse_query(query)
|
|
127
|
+
|
|
128
|
+
# Ensure configuration is initialized (can be dummy if just reading files)
|
|
129
|
+
begin
|
|
130
|
+
DhanHQ.configure_with_env
|
|
131
|
+
rescue StandardError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
results = if parsed[:type] == :equity
|
|
136
|
+
search_equity(parsed[:name])
|
|
137
|
+
else
|
|
138
|
+
search_derivative(
|
|
139
|
+
parsed[:underlying],
|
|
140
|
+
strike: parsed[:strike],
|
|
141
|
+
option_type: parsed[:option_type],
|
|
142
|
+
expiry: parsed[:expiry]
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if results.empty?
|
|
147
|
+
puts "No instruments found for: #{query}"
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
puts "\nResults for: #{query}\n\n"
|
|
152
|
+
printf("%-15s | %-18s | %-12s | %-12s | %-6s | %-10s\n", "Security ID", "Trading Symbol", "Exchange Seg", "Instrument", "Lot", "Expiry")
|
|
153
|
+
puts "-" * 85
|
|
154
|
+
results.each do |row|
|
|
155
|
+
printf(
|
|
156
|
+
"%-15s | %-18s | %-12s | %-12s | %-6d | %-10s\n",
|
|
157
|
+
row.security_id.to_s,
|
|
158
|
+
row.symbol_name.to_s,
|
|
159
|
+
row.exchange_segment.to_s,
|
|
160
|
+
row.instrument.to_s,
|
|
161
|
+
row.lot_size.to_i,
|
|
162
|
+
row.expiry_date.to_s
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
puts
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
main if __FILE__ == $PROGRAM_NAME
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rubocop:disable Style/FormatStringToken
|
|
3
|
+
# rubocop:disable Naming/AccessorMethodName
|
|
4
|
+
|
|
5
|
+
require "json"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require "date"
|
|
8
|
+
|
|
9
|
+
def _get_log_path
|
|
10
|
+
plugin_data = ENV.fetch("CLAUDE_PLUGIN_DATA", nil)
|
|
11
|
+
if plugin_data && !plugin_data.empty?
|
|
12
|
+
FileUtils.mkdir_p(plugin_data)
|
|
13
|
+
File.join(plugin_data, "trades.jsonl")
|
|
14
|
+
else
|
|
15
|
+
default_dir = File.expand_path("../data", __dir__)
|
|
16
|
+
FileUtils.mkdir_p(default_dir)
|
|
17
|
+
File.join(default_dir, "trades.jsonl")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log_order(order_params, response, notes = "")
|
|
22
|
+
log_path = _get_log_path
|
|
23
|
+
FileUtils.mkdir_p(File.dirname(log_path))
|
|
24
|
+
|
|
25
|
+
status = response[:status] || response["status"]
|
|
26
|
+
data = response[:data] || response["data"]
|
|
27
|
+
order_id = data.is_a?(Hash) ? (data[:orderId] || data["orderId"]) : nil
|
|
28
|
+
|
|
29
|
+
record = {
|
|
30
|
+
"timestamp" => Time.now.strftime("%Y-%m-%dT%H:%M:%S%:z"),
|
|
31
|
+
"order_params" => order_params,
|
|
32
|
+
"response" => response,
|
|
33
|
+
"order_id" => order_id,
|
|
34
|
+
"status" => status,
|
|
35
|
+
"notes" => notes
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
File.open(log_path, "a") do |f|
|
|
39
|
+
f.write("#{JSON.dump(record)}\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
record
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def _read_all_records
|
|
46
|
+
log_path = _get_log_path
|
|
47
|
+
return [] unless File.exist?(log_path)
|
|
48
|
+
|
|
49
|
+
records = []
|
|
50
|
+
File.foreach(log_path) do |line|
|
|
51
|
+
line_trimmed = line.strip
|
|
52
|
+
next if line_trimmed.empty?
|
|
53
|
+
|
|
54
|
+
records << JSON.parse(line_trimmed)
|
|
55
|
+
rescue JSON::ParserError
|
|
56
|
+
# skip invalid lines
|
|
57
|
+
end
|
|
58
|
+
records
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_today_orders
|
|
62
|
+
today_str = begin
|
|
63
|
+
Date.today.isoformat
|
|
64
|
+
rescue StandardError
|
|
65
|
+
Date.today.strftime("%Y-%m-%d")
|
|
66
|
+
end
|
|
67
|
+
_read_all_records.select { |r| r["timestamp"].start_with?(today_str) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get_trade_history(days = 7)
|
|
71
|
+
cutoff_time = Time.now - (days * 24 * 60 * 60)
|
|
72
|
+
cutoff_str = cutoff_time.strftime("%Y-%m-%dT%H:%M:%S%:z")
|
|
73
|
+
|
|
74
|
+
records = _read_all_records.select { |r| r["timestamp"] >= cutoff_str }
|
|
75
|
+
records.sort_by { |r| r["timestamp"] }.reverse
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def get_trade_summary(days = 7)
|
|
79
|
+
records = get_trade_history(days)
|
|
80
|
+
summary = {
|
|
81
|
+
"period_days" => days,
|
|
82
|
+
"total_orders" => records.size,
|
|
83
|
+
"successful" => records.count { |r| r["status"] == "success" },
|
|
84
|
+
"failed" => records.count { |r| r["status"] != "success" },
|
|
85
|
+
"buy_count" => 0,
|
|
86
|
+
"sell_count" => 0,
|
|
87
|
+
"instruments_traded" => []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
instruments = []
|
|
91
|
+
records.each do |r|
|
|
92
|
+
params = r["order_params"] || {}
|
|
93
|
+
txn = params["transaction_type"] || params[:transaction_type] || ""
|
|
94
|
+
if txn.to_s.upcase == DhanHQ::Constants::TransactionType::BUY
|
|
95
|
+
summary["buy_count"] += 1
|
|
96
|
+
elsif txn.to_s.upcase == DhanHQ::Constants::TransactionType::SELL
|
|
97
|
+
summary["sell_count"] += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
sid = params["security_id"] || params[:security_id] || params["trading_symbol"] || params[:trading_symbol]
|
|
101
|
+
instruments << sid.to_s if sid
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
summary["instruments_traded"] = instruments.uniq
|
|
105
|
+
summary
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def print_today_orders
|
|
109
|
+
orders = get_today_orders
|
|
110
|
+
if orders.empty?
|
|
111
|
+
puts "No orders placed today."
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
puts "--- Today's Orders (#{orders.size} total) ---"
|
|
116
|
+
orders.each do |r|
|
|
117
|
+
params = r["order_params"] || {}
|
|
118
|
+
status = r["status"] || "unknown"
|
|
119
|
+
oid = r["order_id"] || "N/A"
|
|
120
|
+
txn = params["transaction_type"] || params[:transaction_type] || "?"
|
|
121
|
+
sym = params["trading_symbol"] || params[:trading_symbol] || params["security_id"] || params[:security_id] || "?"
|
|
122
|
+
qty = params["quantity"] || params[:quantity] || "?"
|
|
123
|
+
price = params["price"] || params[:price] || "MKT"
|
|
124
|
+
|
|
125
|
+
time_part = r["timestamp"].split("T")[1] ? r["timestamp"].split("T")[1][0...8] : "00:00:00"
|
|
126
|
+
|
|
127
|
+
printf(" [%s] %-8s | %-4s %dx %s @ %s | ID: %s\n", time_part, status.to_s.upcase, txn.to_s.upcase, qty, sym, price, oid)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
print_today_orders if __FILE__ == $PROGRAM_NAME
|