DhanHQ 3.0.1 → 3.1.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 +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +51 -0
- data/README.md +152 -4
- data/docs/CONSTANTS_REFERENCE.md +3 -2
- data/exe/dhanhq-mcp +7 -0
- data/lib/DhanHQ/agent/tool_registry.rb +51 -2
- data/lib/DhanHQ/concerns/order_audit.rb +43 -1
- data/lib/DhanHQ/constants.rb +3 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
- data/lib/DhanHQ/mcp/server.rb +172 -9
- data/lib/DhanHQ/models/instrument.rb +44 -14
- data/lib/DhanHQ/rate_limiter.rb +5 -3
- data/lib/DhanHQ/resources/alert_orders.rb +1 -0
- data/lib/DhanHQ/resources/forever_orders.rb +1 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +1 -0
- data/lib/DhanHQ/resources/orders.rb +2 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
- data/lib/DhanHQ/resources/super_orders.rb +1 -0
- data/lib/DhanHQ/resources/twap_orders.rb +1 -0
- data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
- data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
- data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
- data/lib/DhanHQ/risk/pipeline.rb +8 -1
- data/lib/DhanHQ/skills/base.rb +54 -3
- data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +10 -13
- data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +15 -19
- data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
- data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +5 -8
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +8 -6
- data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +13 -13
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/dhan_hq.rb +47 -0
- data/skills/dhanhq-ruby/SKILL.md +174 -41
- 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 +105 -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 +85 -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 +200 -6
- 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 +39 -3
- data/skills/dhanhq-ruby/references/market_data.md +0 -3
|
@@ -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
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# rubocop:disable Metrics/AbcSize
|
|
4
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
5
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
6
|
+
# rubocop:disable Metrics/ParameterLists
|
|
7
|
+
# rubocop:disable Style/FormatString
|
|
8
|
+
|
|
9
|
+
require "date"
|
|
10
|
+
|
|
11
|
+
LOT_SIZES = {
|
|
12
|
+
"NIFTY" => 75,
|
|
13
|
+
"BANKNIFTY" => 15,
|
|
14
|
+
"FINNIFTY" => 25,
|
|
15
|
+
"MIDCPNIFTY" => 50,
|
|
16
|
+
"SENSEX" => 10
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
FREEZE_QTY = {
|
|
20
|
+
"NIFTY" => 1800,
|
|
21
|
+
"BANKNIFTY" => 900,
|
|
22
|
+
"FINNIFTY" => 1000,
|
|
23
|
+
"MIDCPNIFTY" => 2800,
|
|
24
|
+
"SENSEX" => 500
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
VALID_EXCHANGE_SEGMENTS = %w[
|
|
28
|
+
NSE_EQ BSE_EQ NSE_FNO BSE_FNO MCX_COMM NSE_CURRENCY BSE_CURRENCY
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
EQUITY_SEGMENTS = %w[NSE_EQ BSE_EQ].freeze
|
|
32
|
+
DERIVATIVE_SEGMENTS = %w[NSE_FNO BSE_FNO MCX_COMM NSE_CURRENCY BSE_CURRENCY].freeze
|
|
33
|
+
|
|
34
|
+
EQUITY_PRODUCT_TYPES = %w[CNC INTRADAY MARGIN MTF].freeze
|
|
35
|
+
DERIVATIVE_PRODUCT_TYPES = %w[INTRADAY MARGIN].freeze
|
|
36
|
+
|
|
37
|
+
VALID_ORDER_TYPES = %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET].freeze
|
|
38
|
+
VALID_TRANSACTION_TYPES = %w[BUY SELL].freeze
|
|
39
|
+
VALID_VALIDITY = %w[DAY IOC].freeze
|
|
40
|
+
|
|
41
|
+
NOTIONAL_WARNING_THRESHOLD = 50_000
|
|
42
|
+
|
|
43
|
+
def _infer_lot_size(trading_symbol)
|
|
44
|
+
return nil if trading_symbol.nil? || trading_symbol.to_s.empty?
|
|
45
|
+
|
|
46
|
+
symbol_upper = trading_symbol.to_s.upcase
|
|
47
|
+
LOT_SIZES.each do |name, lot_size|
|
|
48
|
+
return lot_size if symbol_upper.include?(name)
|
|
49
|
+
end
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def _infer_freeze_qty(trading_symbol)
|
|
54
|
+
return nil if trading_symbol.nil? || trading_symbol.to_s.empty?
|
|
55
|
+
|
|
56
|
+
symbol_upper = trading_symbol.to_s.upcase
|
|
57
|
+
FREEZE_QTY.each do |name, freeze_qty|
|
|
58
|
+
return freeze_qty if symbol_upper.include?(name)
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_order(
|
|
64
|
+
security_id: nil,
|
|
65
|
+
exchange_segment: nil,
|
|
66
|
+
transaction_type: nil,
|
|
67
|
+
quantity: nil,
|
|
68
|
+
order_type: nil,
|
|
69
|
+
product_type: nil,
|
|
70
|
+
price: 0.0,
|
|
71
|
+
trigger_price: 0.0,
|
|
72
|
+
validity: DhanHQ::Constants::Validity::DAY,
|
|
73
|
+
after_market_order: false,
|
|
74
|
+
trading_symbol: nil,
|
|
75
|
+
lot_size: nil
|
|
76
|
+
)
|
|
77
|
+
errors = []
|
|
78
|
+
warnings = []
|
|
79
|
+
|
|
80
|
+
exchange_segment = exchange_segment&.to_s&.upcase
|
|
81
|
+
transaction_type = transaction_type&.to_s&.upcase
|
|
82
|
+
order_type = order_type&.to_s&.upcase
|
|
83
|
+
product_type = product_type&.to_s&.upcase
|
|
84
|
+
validity = validity&.to_s&.upcase
|
|
85
|
+
|
|
86
|
+
errors << "security_id is required" if security_id.nil? || security_id.to_s.empty?
|
|
87
|
+
errors << "exchange_segment is required" if exchange_segment.nil? || exchange_segment.to_s.empty?
|
|
88
|
+
errors << "transaction_type is required" if transaction_type.nil? || transaction_type.to_s.empty?
|
|
89
|
+
errors << "quantity must be a positive integer" if quantity.nil? || quantity.to_i <= 0
|
|
90
|
+
errors << "order_type is required" if order_type.nil? || order_type.to_s.empty?
|
|
91
|
+
errors << "product_type is required" if product_type.nil? || product_type.to_s.empty?
|
|
92
|
+
|
|
93
|
+
errors << "Invalid exchange_segment: #{exchange_segment}" if exchange_segment && !VALID_EXCHANGE_SEGMENTS.include?(exchange_segment)
|
|
94
|
+
errors << "Invalid transaction_type: #{transaction_type}" if transaction_type && !VALID_TRANSACTION_TYPES.include?(transaction_type)
|
|
95
|
+
errors << "Invalid order_type: #{order_type}" if order_type && !VALID_ORDER_TYPES.include?(order_type)
|
|
96
|
+
errors << "Invalid validity: #{validity}" if validity && !VALID_VALIDITY.include?(validity)
|
|
97
|
+
|
|
98
|
+
errors << "price is required for #{order_type} orders" if %w[LIMIT STOP_LOSS].include?(order_type) && price.to_f <= 0
|
|
99
|
+
errors << "trigger_price is required for #{order_type} orders" if %w[STOP_LOSS STOP_LOSS_MARKET].include?(order_type) && trigger_price.to_f <= 0
|
|
100
|
+
|
|
101
|
+
if exchange_segment && EQUITY_SEGMENTS.include?(exchange_segment) && product_type && !EQUITY_PRODUCT_TYPES.include?(product_type)
|
|
102
|
+
errors << "Invalid product_type '#{product_type}' for equity segment '#{exchange_segment}'. Valid values: #{EQUITY_PRODUCT_TYPES.sort}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if exchange_segment && DERIVATIVE_SEGMENTS.include?(exchange_segment) && product_type && !DERIVATIVE_PRODUCT_TYPES.include?(product_type)
|
|
106
|
+
errors << "Invalid product_type '#{product_type}' for derivative segment '#{exchange_segment}'. Valid values: #{DERIVATIVE_PRODUCT_TYPES.sort}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
warnings << "Dhan's current order docs say API market orders are converted to limit orders with MPP." if order_type == DhanHQ::Constants::OrderType::MARKET
|
|
110
|
+
|
|
111
|
+
effective_lot_size = lot_size || _infer_lot_size(trading_symbol)
|
|
112
|
+
if exchange_segment && DERIVATIVE_SEGMENTS.include?(exchange_segment) && quantity
|
|
113
|
+
if effective_lot_size && quantity.to_i % effective_lot_size != 0
|
|
114
|
+
errors << "Derivative quantity must be a multiple of lot size #{effective_lot_size}. Got #{quantity}."
|
|
115
|
+
elsif effective_lot_size.nil?
|
|
116
|
+
warnings << "Could not resolve a lot size from the provided data. Confirm lot size from the security master before placing."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
freeze_qty = _infer_freeze_qty(trading_symbol)
|
|
120
|
+
if freeze_qty && quantity.to_i > freeze_qty
|
|
121
|
+
warnings << "Quantity #{quantity} exceeds fallback freeze quantity #{freeze_qty}. Consider place_slice_order() after verifying the latest exchange freeze limits."
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if price.to_f.positive? && quantity
|
|
126
|
+
notional = price.to_f * quantity.to_i
|
|
127
|
+
warnings << "High notional value: Rs. #{"%.2f" % notional} exceeds the Rs. 50,000 warning threshold." if notional > NOTIONAL_WARNING_THRESHOLD
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless after_market_order
|
|
131
|
+
now = Time.now
|
|
132
|
+
if now.saturday? || now.sunday?
|
|
133
|
+
warnings << "Market is closed on weekends. Use AMO only if that is intentional."
|
|
134
|
+
elsif now.hour < 9 || (now.hour == 9 && now.min < 15)
|
|
135
|
+
warnings << "Regular market is not yet open."
|
|
136
|
+
elsif now.hour > 15 || (now.hour == 15 && now.min > 30)
|
|
137
|
+
warnings << "Regular market is closed. Use AMO only if that is intentional."
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
"valid" => errors.empty?,
|
|
143
|
+
"errors" => errors,
|
|
144
|
+
"warnings" => warnings
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def print_validation(result)
|
|
149
|
+
if result["valid"]
|
|
150
|
+
puts "Order validation: PASS"
|
|
151
|
+
else
|
|
152
|
+
puts "Order validation: FAIL"
|
|
153
|
+
result["errors"].each { |error| puts " ERROR: #{error}" }
|
|
154
|
+
end
|
|
155
|
+
result["warnings"].each { |warning| puts " WARNING: #{warning}" }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if __FILE__ == $PROGRAM_NAME
|
|
159
|
+
sample = validate_order(
|
|
160
|
+
security_id: "2885",
|
|
161
|
+
exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
|
|
162
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
163
|
+
quantity: 10,
|
|
164
|
+
order_type: DhanHQ::Constants::OrderType::LIMIT,
|
|
165
|
+
product_type: DhanHQ::Constants::ProductType::CNC,
|
|
166
|
+
price: 2450
|
|
167
|
+
)
|
|
168
|
+
print_validation(sample)
|
|
169
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DhanHQ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -172,6 +172,7 @@ email:
|
|
|
172
172
|
- shubhamtaywade82@gmail.com
|
|
173
173
|
executables:
|
|
174
174
|
- DhanHQ
|
|
175
|
+
- dhanhq-mcp
|
|
175
176
|
extensions: []
|
|
176
177
|
extra_rdoc_files: []
|
|
177
178
|
files:
|
|
@@ -216,6 +217,7 @@ files:
|
|
|
216
217
|
- docs/WEBSOCKET_PROTOCOL.md
|
|
217
218
|
- docs/architecture-overview.svg
|
|
218
219
|
- exe/DhanHQ
|
|
220
|
+
- exe/dhanhq-mcp
|
|
219
221
|
- lib/DhanHQ.rb
|
|
220
222
|
- lib/DhanHQ/agent.rb
|
|
221
223
|
- lib/DhanHQ/agent/order_preview.rb
|
|
@@ -339,19 +341,28 @@ files:
|
|
|
339
341
|
- lib/DhanHQ/resources/twap_orders.rb
|
|
340
342
|
- lib/DhanHQ/risk.rb
|
|
341
343
|
- lib/DhanHQ/risk/checks/asm_gsm.rb
|
|
344
|
+
- lib/DhanHQ/risk/checks/concentration.rb
|
|
342
345
|
- lib/DhanHQ/risk/checks/market_hours.rb
|
|
346
|
+
- lib/DhanHQ/risk/checks/max_loss.rb
|
|
343
347
|
- lib/DhanHQ/risk/checks/options.rb
|
|
344
348
|
- lib/DhanHQ/risk/checks/order_type.rb
|
|
349
|
+
- lib/DhanHQ/risk/checks/position_limits.rb
|
|
345
350
|
- lib/DhanHQ/risk/checks/product_support.rb
|
|
346
351
|
- lib/DhanHQ/risk/checks/quantity.rb
|
|
347
352
|
- lib/DhanHQ/risk/checks/trading_permission.rb
|
|
348
353
|
- lib/DhanHQ/risk/pipeline.rb
|
|
349
354
|
- lib/DhanHQ/skills.rb
|
|
350
355
|
- lib/DhanHQ/skills/base.rb
|
|
356
|
+
- lib/DhanHQ/skills/builtin/bear_call_spread.rb
|
|
357
|
+
- lib/DhanHQ/skills/builtin/bull_put_spread.rb
|
|
351
358
|
- lib/DhanHQ/skills/builtin/buy_atm_call.rb
|
|
359
|
+
- lib/DhanHQ/skills/builtin/covered_call.rb
|
|
352
360
|
- lib/DhanHQ/skills/builtin/iron_condor.rb
|
|
361
|
+
- lib/DhanHQ/skills/builtin/market_data_summarizer.rb
|
|
362
|
+
- lib/DhanHQ/skills/builtin/protective_put.rb
|
|
353
363
|
- lib/DhanHQ/skills/builtin/square_off_all.rb
|
|
354
364
|
- lib/DhanHQ/skills/builtin/square_off_position.rb
|
|
365
|
+
- lib/DhanHQ/skills/builtin/straddle.rb
|
|
355
366
|
- lib/DhanHQ/skills/builtin/strangle.rb
|
|
356
367
|
- lib/DhanHQ/skills/registry.rb
|
|
357
368
|
- lib/DhanHQ/skills/workflow.rb
|
|
@@ -406,8 +417,33 @@ files:
|
|
|
406
417
|
- lib/ta/technical_analysis.rb
|
|
407
418
|
- sig/DhanHQ.rbs
|
|
408
419
|
- skills/dhanhq-ruby/SKILL.md
|
|
409
|
-
- skills/dhanhq-ruby/
|
|
420
|
+
- skills/dhanhq-ruby/examples/fetch_option_chain.rb
|
|
421
|
+
- skills/dhanhq-ruby/examples/gtt_forever_order.rb
|
|
422
|
+
- skills/dhanhq-ruby/examples/historical_data_analysis.rb
|
|
423
|
+
- skills/dhanhq-ruby/examples/iron_condor.rb
|
|
424
|
+
- skills/dhanhq-ruby/examples/live_feed_setup.rb
|
|
425
|
+
- skills/dhanhq-ruby/examples/margin_check.rb
|
|
426
|
+
- skills/dhanhq-ruby/examples/order_management.rb
|
|
427
|
+
- skills/dhanhq-ruby/examples/place_equity_order.rb
|
|
428
|
+
- skills/dhanhq-ruby/examples/place_fno_order.rb
|
|
429
|
+
- skills/dhanhq-ruby/examples/portfolio_summary.rb
|
|
430
|
+
- skills/dhanhq-ruby/examples/super_order_with_sl.rb
|
|
431
|
+
- skills/dhanhq-ruby/references/backtesting-with-dhan.md
|
|
432
|
+
- skills/dhanhq-ruby/references/common-workflows.md
|
|
433
|
+
- skills/dhanhq-ruby/references/error-codes.md
|
|
434
|
+
- skills/dhanhq-ruby/references/funds.md
|
|
435
|
+
- skills/dhanhq-ruby/references/instruments.md
|
|
436
|
+
- skills/dhanhq-ruby/references/live-feed.md
|
|
437
|
+
- skills/dhanhq-ruby/references/market-data.md
|
|
438
|
+
- skills/dhanhq-ruby/references/option-chain.md
|
|
439
|
+
- skills/dhanhq-ruby/references/options-analysis-patterns.md
|
|
410
440
|
- skills/dhanhq-ruby/references/orders.md
|
|
441
|
+
- skills/dhanhq-ruby/references/portfolio.md
|
|
442
|
+
- skills/dhanhq-ruby/references/scanx-data.md
|
|
443
|
+
- skills/dhanhq-ruby/scripts/dhan_helpers.rb
|
|
444
|
+
- skills/dhanhq-ruby/scripts/resolve_security.rb
|
|
445
|
+
- skills/dhanhq-ruby/scripts/trade_logger.rb
|
|
446
|
+
- skills/dhanhq-ruby/scripts/validate_order.rb
|
|
411
447
|
- watchlist.csv
|
|
412
448
|
homepage: https://github.com/shubhamtaywade82/dhanhq-client
|
|
413
449
|
licenses:
|