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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +185 -0
- data/CHANGELOG.md +31 -0
- data/GUIDE.md +173 -31
- data/README.md +437 -133
- data/README1.md +267 -26
- data/docs/live_order_updates.md +319 -0
- data/docs/rails_integration.md +1 -1
- data/docs/rails_websocket_integration.md +847 -0
- data/docs/standalone_ruby_websocket_integration.md +1588 -0
- data/docs/technical_analysis.md +1 -0
- data/docs/websocket_integration.md +871 -0
- data/examples/comprehensive_websocket_examples.rb +148 -0
- data/examples/instrument_finder_test.rb +195 -0
- data/examples/live_order_updates.rb +118 -0
- data/examples/market_depth_example.rb +144 -0
- data/examples/market_feed_example.rb +81 -0
- data/examples/order_update_example.rb +105 -0
- data/examples/trading_fields_example.rb +215 -0
- data/lib/DhanHQ/config.rb +1 -0
- data/lib/DhanHQ/configuration.rb +16 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
- data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
- data/lib/DhanHQ/errors.rb +2 -0
- data/lib/DhanHQ/models/expired_options_data.rb +331 -0
- data/lib/DhanHQ/models/instrument.rb +96 -2
- data/lib/DhanHQ/models/option_chain.rb +2 -0
- data/lib/DhanHQ/models/order_update.rb +235 -0
- data/lib/DhanHQ/models/trade.rb +118 -31
- data/lib/DhanHQ/rate_limiter.rb +4 -2
- data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +249 -0
- data/lib/DhanHQ/ws/client.rb +1 -1
- data/lib/DhanHQ/ws/connection.rb +3 -3
- data/lib/DhanHQ/ws/decoder.rb +3 -3
- data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
- data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
- data/lib/DhanHQ/ws/market_depth.rb +74 -0
- data/lib/DhanHQ/ws/orders/client.rb +177 -10
- data/lib/DhanHQ/ws/orders/connection.rb +41 -83
- data/lib/DhanHQ/ws/orders.rb +31 -2
- data/lib/DhanHQ/ws/registry.rb +1 -0
- data/lib/DhanHQ/ws/segments.rb +21 -5
- data/lib/DhanHQ/ws/sub_state.rb +1 -1
- data/lib/DhanHQ/ws.rb +3 -2
- data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
- data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
- data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
- data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
- data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
- data/lib/ta/candles.rb +1 -0
- data/lib/ta/fetcher.rb +1 -0
- data/lib/ta/indicators.rb +2 -1
- data/lib/ta/market_calendar.rb +4 -3
- data/lib/ta/technical_analysis.rb +3 -2
- metadata +38 -4
- data/lib/DhanHQ/ws/errors.rb +0 -0
- /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
data/lib/DhanHQ/configuration.rb
CHANGED
@@ -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
|
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
|
@@ -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
|
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
|