DhanHQ 2.1.3 → 2.1.6

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +185 -0
  4. data/CHANGELOG.md +31 -0
  5. data/GUIDE.md +173 -31
  6. data/README.md +437 -133
  7. data/README1.md +267 -26
  8. data/docs/live_order_updates.md +319 -0
  9. data/docs/rails_integration.md +1 -1
  10. data/docs/rails_websocket_integration.md +847 -0
  11. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  12. data/docs/technical_analysis.md +1 -0
  13. data/docs/websocket_integration.md +871 -0
  14. data/examples/comprehensive_websocket_examples.rb +148 -0
  15. data/examples/instrument_finder_test.rb +195 -0
  16. data/examples/live_order_updates.rb +118 -0
  17. data/examples/market_depth_example.rb +144 -0
  18. data/examples/market_feed_example.rb +81 -0
  19. data/examples/order_update_example.rb +105 -0
  20. data/examples/trading_fields_example.rb +215 -0
  21. data/lib/DhanHQ/config.rb +1 -0
  22. data/lib/DhanHQ/configuration.rb +16 -1
  23. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  24. data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
  25. data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
  26. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  27. data/lib/DhanHQ/errors.rb +2 -0
  28. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  29. data/lib/DhanHQ/models/instrument.rb +96 -2
  30. data/lib/DhanHQ/models/option_chain.rb +2 -0
  31. data/lib/DhanHQ/models/order_update.rb +235 -0
  32. data/lib/DhanHQ/models/trade.rb +118 -31
  33. data/lib/DhanHQ/rate_limiter.rb +4 -2
  34. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  35. data/lib/DhanHQ/version.rb +1 -1
  36. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  37. data/lib/DhanHQ/ws/client.rb +1 -1
  38. data/lib/DhanHQ/ws/connection.rb +3 -3
  39. data/lib/DhanHQ/ws/decoder.rb +3 -3
  40. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  41. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  42. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  43. data/lib/DhanHQ/ws/orders/client.rb +177 -10
  44. data/lib/DhanHQ/ws/orders/connection.rb +41 -83
  45. data/lib/DhanHQ/ws/orders.rb +31 -2
  46. data/lib/DhanHQ/ws/registry.rb +1 -0
  47. data/lib/DhanHQ/ws/segments.rb +21 -5
  48. data/lib/DhanHQ/ws/sub_state.rb +1 -1
  49. data/lib/DhanHQ/ws.rb +3 -2
  50. data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
  51. data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
  52. data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
  53. data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
  54. data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
  55. data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
  56. data/lib/ta/candles.rb +1 -0
  57. data/lib/ta/fetcher.rb +1 -0
  58. data/lib/ta/indicators.rb +2 -1
  59. data/lib/ta/market_calendar.rb +4 -3
  60. data/lib/ta/technical_analysis.rb +3 -2
  61. metadata +38 -4
  62. data/lib/DhanHQ/ws/errors.rb +0 -0
  63. /data/lib/DhanHQ/contracts/{modify_order_contract copy.rb → modify_order_contract_copy.rb} +0 -0
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Order Update WebSocket Example
5
+ # This script demonstrates how to use the DhanHQ Order Update WebSocket
6
+ # Receives real-time order status updates and execution notifications
7
+ # NOTE: Uses a SINGLE connection to avoid rate limiting
8
+
9
+ require "dhan_hq"
10
+
11
+ # Configure DhanHQ
12
+ DhanHQ.configure do |config|
13
+ config.client_id = ENV["CLIENT_ID"] || "your_client_id"
14
+ config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
15
+ config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
16
+ end
17
+
18
+ puts "DhanHQ Order Update WebSocket Example"
19
+ puts "====================================="
20
+ puts "Receives real-time order updates including:"
21
+ puts "- Order status changes"
22
+ puts "- Execution notifications"
23
+ puts "- Order rejections"
24
+ puts "- Trade confirmations"
25
+ puts ""
26
+ puts "NOTE: Using SINGLE connection to avoid rate limiting (429 errors)"
27
+ puts "Dhan allows up to 5 WebSocket connections per user"
28
+ puts ""
29
+
30
+ # Order Update WebSocket Connection
31
+ puts "Order Update WebSocket Connection"
32
+ puts "================================="
33
+
34
+ # Create a single order update WebSocket connection
35
+ puts "Creating Order Update WebSocket connection..."
36
+ orders_client = DhanHQ::WS::Orders.connect do |order_update|
37
+ puts "Order Update: #{order_update.order_no} - #{order_update.status}"
38
+ puts " Symbol: #{order_update.symbol}"
39
+ puts " Quantity: #{order_update.quantity}"
40
+ puts " Traded Qty: #{order_update.traded_qty}"
41
+ puts " Price: #{order_update.price}"
42
+ puts " Execution: #{order_update.execution_percentage}%"
43
+ puts " ---"
44
+ end
45
+
46
+ # Add event handlers for different order events
47
+ puts "\nSetting up event handlers..."
48
+
49
+ orders_client.on(:update) do |order_update|
50
+ puts "📝 Order Updated: #{order_update.order_no} - #{order_update.status}"
51
+ end
52
+
53
+ orders_client.on(:status_change) do |change_data|
54
+ puts "🔄 Status Changed: #{change_data[:previous_status]} -> #{change_data[:new_status]}"
55
+ end
56
+
57
+ orders_client.on(:execution) do |execution_data|
58
+ puts "✅ Execution: #{execution_data[:new_traded_qty]} shares executed"
59
+ end
60
+
61
+ orders_client.on(:order_traded) do |order_update|
62
+ puts "💰 Order Traded: #{order_update.order_no} - #{order_update.symbol}"
63
+ end
64
+
65
+ orders_client.on(:order_rejected) do |order_update|
66
+ puts "❌ Order Rejected: #{order_update.order_no} - #{order_update.reason_description}"
67
+ end
68
+
69
+ orders_client.on(:error) do |error|
70
+ puts "⚠️ WebSocket Error: #{error}"
71
+ end
72
+
73
+ orders_client.on(:close) do |close_info|
74
+ if close_info.is_a?(Hash)
75
+ puts "🔌 WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}"
76
+ else
77
+ puts "🔌 WebSocket Closed: #{close_info}"
78
+ end
79
+ end
80
+
81
+ puts "\nOrder Update WebSocket connected successfully!"
82
+ puts "Waiting 30 seconds to receive order updates..."
83
+ puts "Press Ctrl+C to stop early"
84
+ puts ""
85
+
86
+ # Wait for order updates
87
+ begin
88
+ sleep(30)
89
+ rescue Interrupt
90
+ puts "\nStopping early due to user interrupt..."
91
+ end
92
+
93
+ # Graceful shutdown
94
+ puts "\nShutting down Order Update WebSocket connection..."
95
+ orders_client.stop
96
+
97
+ puts "Order Update WebSocket connection closed."
98
+ puts "Example completed!"
99
+ puts ""
100
+ puts "Summary:"
101
+ puts "- Successfully demonstrated Order Update WebSocket"
102
+ puts "- Real-time order status tracking"
103
+ puts "- Multiple event handlers for different order events"
104
+ puts "- Used single connection to avoid rate limiting (429 errors)"
105
+ puts "- Proper connection cleanup prevents resource leaks"
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # DhanHQ Trading Fields Example
5
+ # This script demonstrates how to use the new trading fields in the Instrument model
6
+ # for practical trading operations and risk management
7
+
8
+ require "dhan_hq"
9
+
10
+ # Configure DhanHQ
11
+ DhanHQ.configure do |config|
12
+ config.client_id = ENV["CLIENT_ID"] || "your_client_id"
13
+ config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
14
+ config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
15
+ end
16
+
17
+ puts "DhanHQ Trading Fields Example"
18
+ puts "============================="
19
+ puts "Demonstrating essential trading fields for risk management and order validation"
20
+ puts ""
21
+
22
+ # Helper method to display trading information
23
+ def display_trading_info(instrument, name)
24
+ return unless instrument
25
+
26
+ puts "✅ #{name} Trading Information:"
27
+ puts " Symbol: #{instrument.symbol_name}"
28
+ puts " Underlying Symbol: #{instrument.underlying_symbol}" if instrument.underlying_symbol
29
+ puts " Security ID: #{instrument.security_id}"
30
+ puts " ISIN: #{instrument.isin}"
31
+ puts " Instrument Type: #{instrument.instrument_type}"
32
+ puts " Exchange Segment: #{instrument.exchange_segment}"
33
+ puts " Lot Size: #{instrument.lot_size}"
34
+ puts " Tick Size: #{instrument.tick_size}"
35
+ puts " Expiry Flag: #{instrument.expiry_flag}"
36
+ puts " Bracket Flag: #{instrument.bracket_flag}"
37
+ puts " Cover Flag: #{instrument.cover_flag}"
38
+ puts " ASM/GSM Flag: #{instrument.asm_gsm_flag}"
39
+ puts " ASM/GSM Category: #{instrument.asm_gsm_category}" if instrument.asm_gsm_category != "NA"
40
+ puts " Buy/Sell Indicator: #{instrument.buy_sell_indicator}"
41
+ puts " Buy CO Min Margin %: #{instrument.buy_co_min_margin_per}"
42
+ puts " Sell CO Min Margin %: #{instrument.sell_co_min_margin_per}"
43
+ puts " MTF Leverage: #{instrument.mtf_leverage}"
44
+ puts ""
45
+ end
46
+
47
+ # Helper method to check trading eligibility
48
+ def check_trading_eligibility(instrument, name)
49
+ return unless instrument
50
+
51
+ puts "🔍 #{name} Trading Eligibility Check:"
52
+
53
+ # Check if instrument allows trading
54
+ if instrument.buy_sell_indicator == "A"
55
+ puts " ✅ Trading Allowed"
56
+ else
57
+ puts " ❌ Trading Not Allowed"
58
+ return
59
+ end
60
+
61
+ # Check bracket orders
62
+ if instrument.bracket_flag == "Y"
63
+ puts " ✅ Bracket Orders Allowed"
64
+ else
65
+ puts " ❌ Bracket Orders Not Allowed"
66
+ end
67
+
68
+ # Check cover orders
69
+ if instrument.cover_flag == "Y"
70
+ puts " ✅ Cover Orders Allowed"
71
+ else
72
+ puts " ❌ Cover Orders Not Allowed"
73
+ end
74
+
75
+ # Check ASM/GSM status
76
+ if instrument.asm_gsm_flag == "Y"
77
+ puts " ⚠️ ASM/GSM Applied: #{instrument.asm_gsm_category}"
78
+ else
79
+ puts " ✅ No ASM/GSM Restrictions"
80
+ end
81
+
82
+ # Check expiry
83
+ if instrument.expiry_flag == "Y"
84
+ puts " ⚠️ Instrument Has Expiry: #{instrument.expiry_date}"
85
+ else
86
+ puts " ✅ No Expiry (Perpetual)"
87
+ end
88
+
89
+ puts ""
90
+ end
91
+
92
+ # Helper method to calculate margin requirements
93
+ def calculate_margin_requirements(instrument, name, quantity, price)
94
+ return unless instrument
95
+
96
+ puts "💰 #{name} Margin Calculation:"
97
+ puts " Quantity: #{quantity}"
98
+ puts " Price: ₹#{price}"
99
+ puts " Lot Size: #{instrument.lot_size}"
100
+
101
+ total_value = quantity * price * instrument.lot_size
102
+ puts " Total Value: ₹#{total_value}"
103
+
104
+ # Calculate margin requirements
105
+ buy_margin = total_value * (instrument.buy_co_min_margin_per / 100.0)
106
+ sell_margin = total_value * (instrument.sell_co_min_margin_per / 100.0)
107
+
108
+ puts " Buy CO Margin Required: ₹#{buy_margin}"
109
+ puts " Sell CO Margin Required: ₹#{sell_margin}"
110
+
111
+ # Calculate MTF leverage
112
+ if instrument.mtf_leverage > 0
113
+ mtf_value = total_value * instrument.mtf_leverage
114
+ puts " MTF Leverage: #{instrument.mtf_leverage}x"
115
+ puts " MTF Value: ₹#{mtf_value}"
116
+ end
117
+
118
+ puts ""
119
+ end
120
+
121
+ puts "1. Finding Instruments with Trading Fields"
122
+ puts "=" * 50
123
+
124
+ # Find popular trading instruments
125
+ reliance = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
126
+ tcs = DhanHQ::Models::Instrument.find("NSE_EQ", "TCS")
127
+ hdfc = DhanHQ::Models::Instrument.find("NSE_EQ", "HDFC")
128
+ nifty = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
129
+ banknifty = DhanHQ::Models::Instrument.find("IDX_I", "BANKNIFTY")
130
+
131
+ # Display trading information
132
+ display_trading_info(reliance, "RELIANCE")
133
+ display_trading_info(tcs, "TCS")
134
+ display_trading_info(hdfc, "HDFC")
135
+ display_trading_info(nifty, "NIFTY")
136
+ display_trading_info(banknifty, "BANKNIFTY")
137
+
138
+ puts "2. Trading Eligibility Checks"
139
+ puts "=" * 40
140
+
141
+ # Check trading eligibility
142
+ check_trading_eligibility(reliance, "RELIANCE")
143
+ check_trading_eligibility(tcs, "TCS")
144
+ check_trading_eligibility(nifty, "NIFTY")
145
+
146
+ puts "3. Margin Calculations"
147
+ puts "=" * 25
148
+
149
+ # Calculate margin requirements for different scenarios
150
+ calculate_margin_requirements(reliance, "RELIANCE", 10, 2500)
151
+ calculate_margin_requirements(tcs, "TCS", 5, 3500)
152
+ calculate_margin_requirements(nifty, "NIFTY", 1, 20_000)
153
+
154
+ puts "4. Practical Trading Scenarios"
155
+ puts "=" * 35
156
+
157
+ # Scenario 1: Check if instrument supports bracket orders
158
+ puts "Scenario 1: Bracket Order Support"
159
+ puts "-" * 30
160
+ instruments = [reliance, tcs, hdfc, nifty, banknifty]
161
+ instruments.each do |instrument|
162
+ next unless instrument
163
+
164
+ puts "#{instrument.underlying_symbol || instrument.symbol_name}: #{instrument.bracket_flag == "Y" ? "✅ Supports" : "❌ No Support"}"
165
+ end
166
+ puts ""
167
+
168
+ # Scenario 2: Find instruments with ASM/GSM restrictions
169
+ puts "Scenario 2: ASM/GSM Restricted Instruments"
170
+ puts "-" * 40
171
+ asm_instruments = instruments.select { |i| i&.asm_gsm_flag == "Y" }
172
+ if asm_instruments.any?
173
+ asm_instruments.each do |instrument|
174
+ puts "#{instrument.underlying_symbol || instrument.symbol_name}: #{instrument.asm_gsm_category}"
175
+ end
176
+ else
177
+ puts "No ASM/GSM restricted instruments found in sample"
178
+ end
179
+ puts ""
180
+
181
+ # Scenario 3: Find instruments with high MTF leverage
182
+ puts "Scenario 3: High MTF Leverage Instruments"
183
+ puts "-" * 40
184
+ high_leverage = instruments.select { |i| i&.mtf_leverage && i.mtf_leverage > 3.0 }
185
+ if high_leverage.any?
186
+ high_leverage.each do |instrument|
187
+ puts "#{instrument.underlying_symbol || instrument.symbol_name}: #{instrument.mtf_leverage}x leverage"
188
+ end
189
+ else
190
+ puts "No high leverage instruments found in sample"
191
+ end
192
+ puts ""
193
+
194
+ puts "5. Trading Field Summary"
195
+ puts "=" * 25
196
+ puts "Essential trading fields now available:"
197
+ puts "✅ ISIN - International Securities Identification Number"
198
+ puts "✅ Instrument Type - Classification (ES, INDEX, etc.)"
199
+ puts "✅ Expiry Flag - Whether instrument has expiry"
200
+ puts "✅ Bracket Flag - Bracket order support"
201
+ puts "✅ Cover Flag - Cover order support"
202
+ puts "✅ ASM/GSM Flag - Additional Surveillance Measure status"
203
+ puts "✅ Buy/Sell Indicator - Trading permission"
204
+ puts "✅ Margin Requirements - CO minimum margin percentages"
205
+ puts "✅ MTF Leverage - Margin Trading Facility leverage"
206
+ puts ""
207
+
208
+ puts "Example completed!"
209
+ puts "=================="
210
+ puts "These trading fields enable:"
211
+ puts "- Order validation and eligibility checks"
212
+ puts "- Margin requirement calculations"
213
+ puts "- Risk management and compliance"
214
+ puts "- Trading strategy implementation"
215
+ puts "- Regulatory compliance monitoring"
data/lib/DhanHQ/config.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "logger"
4
4
 
5
+ # DhanHQ Ruby SDK for trading and market data
5
6
  module DhanHQ
6
7
  class << self
7
8
  # keep whatever you already have; add these if missing:
@@ -40,6 +40,18 @@ module DhanHQ
40
40
  # @return [String]
41
41
  attr_accessor :ws_order_url
42
42
 
43
+ # Websocket market feed endpoint.
44
+ # @return [String]
45
+ attr_accessor :ws_market_feed_url
46
+
47
+ # Websocket market depth endpoint.
48
+ # @return [String]
49
+ attr_accessor :ws_market_depth_url
50
+
51
+ # Market depth level (20 or 200).
52
+ # @return [Integer]
53
+ attr_accessor :market_depth_level
54
+
43
55
  # Websocket user type for order updates.
44
56
  # @return [String] "SELF" or "PARTNER".
45
57
  attr_accessor :ws_user_type
@@ -63,7 +75,10 @@ module DhanHQ
63
75
  @access_token = ENV.fetch("ACCESS_TOKEN", nil)
64
76
  @base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
65
77
  @ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
66
- @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
78
+ @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
79
+ @ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", "wss://api-feed.dhan.co")
80
+ @ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", "wss://depth-api-feed.dhan.co/twentydepth")
81
+ @market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
67
82
  @ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
68
83
  @partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
69
84
  @partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", nil)
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation"
4
+ require "date"
5
+
6
+ module DhanHQ
7
+ module Contracts
8
+ ##
9
+ # Validation contract for expired options data requests
10
+ class ExpiredOptionsDataContract < BaseContract
11
+ params do
12
+ required(:exchange_segment).filled(:string)
13
+ required(:interval).filled(:integer)
14
+ required(:security_id).filled(:string)
15
+ required(:instrument).filled(:string)
16
+ required(:expiry_flag).filled(:string)
17
+ required(:expiry_code).filled(:integer)
18
+ required(:strike).filled(:string)
19
+ required(:drv_option_type).filled(:string)
20
+ required(:required_data).filled(:array)
21
+ required(:from_date).filled(:string)
22
+ required(:to_date).filled(:string)
23
+ end
24
+
25
+ rule(:exchange_segment) do
26
+ valid_segments = %w[NSE_FNO BSE_FNO NSE_EQ BSE_EQ]
27
+ key.failure("must be one of: #{valid_segments.join(", ")}") unless valid_segments.include?(value)
28
+ end
29
+
30
+ rule(:interval) do
31
+ valid_intervals = [1, 5, 15, 25, 60]
32
+ key.failure("must be one of: #{valid_intervals.join(", ")}") unless valid_intervals.include?(value)
33
+ end
34
+
35
+ rule(:instrument) do
36
+ valid_instruments = %w[OPTIDX OPTSTK]
37
+ key.failure("must be one of: #{valid_instruments.join(", ")}") unless valid_instruments.include?(value)
38
+ end
39
+
40
+ rule(:expiry_flag) do
41
+ valid_flags = %w[WEEK MONTH]
42
+ key.failure("must be one of: #{valid_flags.join(", ")}") unless valid_flags.include?(value)
43
+ end
44
+
45
+ rule(:strike) do
46
+ unless value.match?(/\AATM(\+|-)?\d*\z/) || value == "ATM"
47
+ key.failure("must be in format ATM, ATM+1, ATM-1, etc. " \
48
+ "(up to ATM+10/ATM-10 for Index Options, ATM+3/ATM-3 for others)")
49
+ end
50
+ end
51
+
52
+ rule(:drv_option_type) do
53
+ valid_types = %w[CALL PUT]
54
+ key.failure("must be one of: #{valid_types.join(", ")}") unless valid_types.include?(value)
55
+ end
56
+
57
+ rule(:required_data) do
58
+ valid_data_types = %w[open high low close iv volume strike oi spot]
59
+ invalid_types = value - valid_data_types
60
+ if invalid_types.any?
61
+ key.failure("contains invalid data types: #{invalid_types.join(", ")}. " \
62
+ "Valid types: #{valid_data_types.join(", ")}")
63
+ end
64
+ end
65
+
66
+ rule(:from_date, :to_date) do
67
+ if valid_date_format?(values[:from_date]) && valid_date_format?(values[:to_date])
68
+ begin
69
+ from_date = Date.parse(values[:from_date])
70
+ to_date = Date.parse(values[:to_date])
71
+
72
+ key.failure("from_date must be before to_date") if from_date >= to_date
73
+
74
+ # Check if date range is not too large (max 30 days)
75
+ key.failure("date range cannot exceed 30 days") if (to_date - from_date).to_i > 30
76
+
77
+ # Check if from_date is not too far in the past (max 5 years)
78
+ five_years_ago = Date.today - (5 * 365)
79
+ key.failure("from_date cannot be more than 5 years ago") if from_date < five_years_ago
80
+ rescue Date::Error
81
+ key.failure("invalid date format")
82
+ end
83
+ else
84
+ key.failure("must be in YYYY-MM-DD format (e.g., 2021-08-01)")
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def valid_date_format?(date_string)
91
+ return false unless date_string.is_a?(String)
92
+ return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
93
+
94
+ begin
95
+ Date.parse(date_string)
96
+ true
97
+ rescue Date::Error
98
+ false
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Contracts
5
+ # Validation contract for order modification requests
5
6
  class ModifyOrderContract < Dry::Validation::Contract
6
7
  params do
7
8
  required(:dhanClientId).filled(:string)
@@ -6,7 +6,7 @@ module DhanHQ
6
6
  module Contracts
7
7
  # **Validation contract for fetching option chain data**
8
8
  #
9
- # Validates request parameters for fetching option chains & expiry lists.
9
+ # Validates request parameters for fetching option chains.
10
10
  class OptionChainContract < BaseContract
11
11
  params do
12
12
  required(:underlying_scrip).filled(:integer) # Security ID
@@ -27,5 +27,15 @@ module DhanHQ
27
27
  end
28
28
  end
29
29
  end
30
+
31
+ # **Validation contract for fetching option chain expiry list**
32
+ #
33
+ # Validates request parameters for fetching expiry lists (expiry not required).
34
+ class OptionChainExpiryListContract < BaseContract
35
+ params do
36
+ required(:underlying_scrip).filled(:integer) # Security ID
37
+ required(:underlying_seg).filled(:string, included_in?: %w[IDX_I NSE_FNO BSE_FNO MCX_FO])
38
+ end
39
+ end
30
40
  end
31
41
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ ##
6
+ # Validation contract for trade-related operations
7
+ class TradeContract < BaseContract
8
+ # No input validation needed for GET requests
9
+ # These contracts are mainly for documentation and future extensibility
10
+ end
11
+
12
+ ##
13
+ # Validation contract for trade history requests
14
+ class TradeHistoryContract < BaseContract
15
+ params do
16
+ required(:from_date).filled(:string)
17
+ required(:to_date).filled(:string)
18
+ optional(:page).filled(:integer, gteq?: 0)
19
+ end
20
+
21
+ rule(:from_date) do
22
+ key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
23
+ end
24
+
25
+ rule(:to_date) do
26
+ key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
27
+ end
28
+
29
+ rule(:from_date, :to_date) do
30
+ from_date_valid = valid_date_format?(values[:from_date])
31
+ to_date_valid = valid_date_format?(values[:to_date])
32
+
33
+ if values[:from_date] && values[:to_date] && from_date_valid && to_date_valid
34
+ begin
35
+ from_date = Date.parse(values[:from_date])
36
+ to_date = Date.parse(values[:to_date])
37
+
38
+ key.failure("from_date must be before or equal to to_date") if from_date > to_date
39
+ rescue Date::Error
40
+ # This shouldn't happen since we already validated format, but just in case
41
+ key.failure("invalid date format")
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def valid_date_format?(date_string)
49
+ return false unless date_string.is_a?(String)
50
+ return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
51
+
52
+ # Additional check to ensure it's a valid date
53
+ begin
54
+ Date.parse(date_string)
55
+ true
56
+ rescue Date::Error
57
+ false
58
+ end
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Validation contract for trade by order ID requests
64
+ class TradeByOrderIdContract < BaseContract
65
+ params do
66
+ required(:order_id).filled(:string, min_size?: 1)
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/DhanHQ/errors.rb CHANGED
@@ -25,6 +25,8 @@ module DhanHQ
25
25
  class InputExceptionError < Error; end
26
26
  # DH-811, DH-812, DH-813, DH-814
27
27
  class InvalidRequestError < Error; end
28
+ # Validation errors for input parameters
29
+ class ValidationError < Error; end
28
30
 
29
31
  # Order and market data errors
30
32
  class OrderError < Error; end