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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +152 -4
  5. data/docs/CONSTANTS_REFERENCE.md +3 -2
  6. data/exe/dhanhq-mcp +7 -0
  7. data/lib/DhanHQ/agent/tool_registry.rb +51 -2
  8. data/lib/DhanHQ/concerns/order_audit.rb +43 -1
  9. data/lib/DhanHQ/constants.rb +3 -2
  10. data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
  11. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
  12. data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
  13. data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
  14. data/lib/DhanHQ/mcp/server.rb +172 -9
  15. data/lib/DhanHQ/models/instrument.rb +44 -14
  16. data/lib/DhanHQ/rate_limiter.rb +5 -3
  17. data/lib/DhanHQ/resources/alert_orders.rb +1 -0
  18. data/lib/DhanHQ/resources/forever_orders.rb +1 -0
  19. data/lib/DhanHQ/resources/iceberg_orders.rb +1 -0
  20. data/lib/DhanHQ/resources/orders.rb +2 -0
  21. data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
  22. data/lib/DhanHQ/resources/super_orders.rb +1 -0
  23. data/lib/DhanHQ/resources/twap_orders.rb +1 -0
  24. data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
  25. data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
  26. data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
  27. data/lib/DhanHQ/risk/pipeline.rb +8 -1
  28. data/lib/DhanHQ/skills/base.rb +54 -3
  29. data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
  30. data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
  31. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +10 -13
  32. data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
  33. data/lib/DhanHQ/skills/builtin/iron_condor.rb +15 -19
  34. data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
  35. data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
  36. data/lib/DhanHQ/skills/builtin/square_off_all.rb +5 -8
  37. data/lib/DhanHQ/skills/builtin/square_off_position.rb +8 -6
  38. data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
  39. data/lib/DhanHQ/skills/builtin/strangle.rb +13 -13
  40. data/lib/DhanHQ/version.rb +1 -1
  41. data/lib/dhan_hq.rb +47 -0
  42. data/skills/dhanhq-ruby/SKILL.md +174 -41
  43. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  44. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  45. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  46. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  47. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  48. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  49. data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
  50. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  51. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  52. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  53. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  54. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  55. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  56. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  57. data/skills/dhanhq-ruby/references/funds.md +67 -0
  58. data/skills/dhanhq-ruby/references/instruments.md +85 -0
  59. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  60. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  61. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  62. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  63. data/skills/dhanhq-ruby/references/orders.md +200 -6
  64. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  65. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  66. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  67. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  68. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  69. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  70. metadata +39 -3
  71. 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.1
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-08 00:00:00.000000000 Z
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/references/market_data.md
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:
@@ -1,3 +0,0 @@
1
- # Market data with agents
2
-
3
- Use `DhanHQ::Models::Instrument.search` to resolve a query to Dhan security IDs, then call `DhanHQ::Models::MarketFeed.ltp`, `.ohlc`, or `.quote` with an exchange-segment keyed payload.