DhanHQ 2.6.1 → 2.6.3
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 +15 -3
- data/ARCHITECTURE.md +113 -0
- data/CHANGELOG.md +55 -0
- data/README.md +2 -0
- data/Rakefile +3 -1
- data/docs/API_VERIFICATION.md +10 -8
- data/docs/ENDPOINTS_AND_SANDBOX.md +115 -0
- data/lib/DhanHQ/auth.rb +2 -2
- data/lib/DhanHQ/client.rb +72 -51
- data/lib/DhanHQ/configuration.rb +45 -11
- data/lib/DhanHQ/constants.rb +68 -4
- data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
- data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
- data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
- data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
- data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
- data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
- data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
- data/lib/DhanHQ/core/auth_api.rb +1 -1
- data/lib/DhanHQ/core/base_api.rb +10 -9
- data/lib/DhanHQ/core/base_model.rb +4 -1
- data/lib/DhanHQ/core/error_handler.rb +2 -2
- data/lib/DhanHQ/errors.rb +14 -2
- data/lib/DhanHQ/helpers/request_helper.rb +27 -5
- data/lib/DhanHQ/helpers/response_helper.rb +48 -19
- data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
- data/lib/DhanHQ/models/alert_order.rb +6 -2
- data/lib/DhanHQ/models/edis.rb +20 -13
- data/lib/DhanHQ/models/expired_options_data.rb +54 -44
- data/lib/DhanHQ/models/forever_order.rb +17 -7
- data/lib/DhanHQ/models/historical_data.rb +40 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
- data/lib/DhanHQ/models/margin.rb +62 -82
- data/lib/DhanHQ/models/market_feed.rb +14 -3
- data/lib/DhanHQ/models/option_chain.rb +50 -150
- data/lib/DhanHQ/models/order.rb +19 -4
- data/lib/DhanHQ/models/super_order.rb +2 -2
- data/lib/DhanHQ/resources/alert_orders.rb +1 -1
- data/lib/DhanHQ/resources/edis.rb +4 -3
- data/lib/DhanHQ/resources/instruments.rb +3 -2
- data/lib/DhanHQ/resources/ip_setup.rb +4 -1
- data/lib/DhanHQ/resources/kill_switch.rb +7 -1
- data/lib/DhanHQ/resources/orders.rb +1 -1
- data/lib/DhanHQ/resources/super_orders.rb +8 -2
- data/lib/DhanHQ/resources/trader_control.rb +13 -4
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +2 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +16 -8
- data/lib/dhan_hq.rb +37 -32
- data/lib/ta/indicators.rb +15 -18
- metadata +7 -9
- data/CODE_REVIEW_ISSUES.md +0 -397
- data/FIXES_APPLIED.md +0 -373
- data/RELEASING.md +0 -60
- data/REVIEW_SUMMARY.md +0 -120
- data/VERSION_UPDATE.md +0 -82
- data/diagram.md +0 -34
- data/docs/ARCHIVE_README.md +0 -784
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -9,9 +9,12 @@ module DhanHQ
|
|
|
9
9
|
# @see https://dhanhq.co/docs/v2/ DhanHQ API Documentation
|
|
10
10
|
class Configuration
|
|
11
11
|
# Default REST API host used when the base URL is not overridden.
|
|
12
|
-
#
|
|
13
12
|
# @return [String]
|
|
14
|
-
BASE_URL =
|
|
13
|
+
BASE_URL = Constants::Urls::REST_API_BASE
|
|
14
|
+
|
|
15
|
+
# Default Sandbox API host.
|
|
16
|
+
# @return [String]
|
|
17
|
+
SANDBOX_URL = Constants::Urls::SANDBOX_API_BASE
|
|
15
18
|
# The client ID for API authentication.
|
|
16
19
|
# @return [String, nil] The client ID or `nil` if not set.
|
|
17
20
|
attr_accessor :client_id
|
|
@@ -32,8 +35,11 @@ module DhanHQ
|
|
|
32
35
|
attr_accessor :on_token_expired
|
|
33
36
|
|
|
34
37
|
# The base URL for API requests.
|
|
35
|
-
# @return [String]
|
|
36
|
-
|
|
38
|
+
# @return [String]
|
|
39
|
+
attr_writer :base_url
|
|
40
|
+
|
|
41
|
+
# Whether to use the sandbox environment.
|
|
42
|
+
attr_accessor :sandbox
|
|
37
43
|
|
|
38
44
|
# URL for the compact CSV format of instruments.
|
|
39
45
|
# @return [String] URL for compact CSV.
|
|
@@ -48,21 +54,33 @@ module DhanHQ
|
|
|
48
54
|
attr_accessor :ws_version
|
|
49
55
|
|
|
50
56
|
# Websocket order updates endpoint.
|
|
57
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
51
58
|
# @return [String]
|
|
52
|
-
|
|
59
|
+
def ws_order_url
|
|
60
|
+
@ws_order_url || Constants::Urls::WS_ORDER_UPDATE
|
|
61
|
+
end
|
|
53
62
|
|
|
54
63
|
# Websocket market feed endpoint.
|
|
64
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
55
65
|
# @return [String]
|
|
56
|
-
|
|
66
|
+
def ws_market_feed_url
|
|
67
|
+
@ws_market_feed_url || Constants::Urls::WS_MARKET_FEED
|
|
68
|
+
end
|
|
57
69
|
|
|
58
70
|
# Websocket market depth endpoint.
|
|
71
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
59
72
|
# @return [String]
|
|
60
|
-
|
|
73
|
+
def ws_market_depth_url
|
|
74
|
+
@ws_market_depth_url || Constants::Urls::WS_DEPTH_20
|
|
75
|
+
end
|
|
61
76
|
|
|
62
77
|
# Market depth level (20 or 200).
|
|
63
78
|
# @return [Integer]
|
|
64
79
|
attr_accessor :market_depth_level
|
|
65
80
|
|
|
81
|
+
# Setters for websocket URLs
|
|
82
|
+
attr_writer :ws_order_url, :ws_market_feed_url, :ws_market_depth_url
|
|
83
|
+
|
|
66
84
|
# Websocket user type for order updates.
|
|
67
85
|
# @return [String] "SELF" or "PARTNER".
|
|
68
86
|
attr_accessor :ws_user_type
|
|
@@ -91,6 +109,21 @@ module DhanHQ
|
|
|
91
109
|
end
|
|
92
110
|
end
|
|
93
111
|
|
|
112
|
+
# Returns the base URL to use. If {#sandbox} is true and {#base_url}
|
|
113
|
+
# is nil or the default production URL, returns {SANDBOX_URL}.
|
|
114
|
+
# @return [String]
|
|
115
|
+
def base_url
|
|
116
|
+
if sandbox? && (@base_url.nil? || @base_url == BASE_URL)
|
|
117
|
+
SANDBOX_URL
|
|
118
|
+
else
|
|
119
|
+
@base_url || BASE_URL
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sandbox?
|
|
124
|
+
@sandbox == true
|
|
125
|
+
end
|
|
126
|
+
|
|
94
127
|
# Initializes a new configuration instance with default values.
|
|
95
128
|
#
|
|
96
129
|
# @example
|
|
@@ -100,11 +133,12 @@ module DhanHQ
|
|
|
100
133
|
def initialize
|
|
101
134
|
@client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
102
135
|
@access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
|
|
103
|
-
@
|
|
136
|
+
@sandbox = ENV.fetch("DHAN_SANDBOX", "false").to_s.casecmp("true").zero?
|
|
137
|
+
@base_url = ENV.fetch("DHAN_BASE_URL", nil)
|
|
104
138
|
@ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
|
105
|
-
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL",
|
|
106
|
-
@ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL",
|
|
107
|
-
@ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL",
|
|
139
|
+
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
|
|
140
|
+
@ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
|
|
141
|
+
@ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
|
|
108
142
|
@market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
|
|
109
143
|
@ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
|
|
110
144
|
@partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
|
data/lib/DhanHQ/constants.rb
CHANGED
|
@@ -18,6 +18,14 @@ module DhanHQ
|
|
|
18
18
|
BSE_FNO = "BSE_FNO"
|
|
19
19
|
|
|
20
20
|
ALL = [IDX_I, NSE_EQ, NSE_FNO, NSE_CURRENCY, NSE_COMM, BSE_EQ, MCX_COMM, BSE_CURRENCY, BSE_FNO].freeze
|
|
21
|
+
# Segments allowed by POST /v2/margincalculator (single and multi).
|
|
22
|
+
MARGIN_CALC_ALL = [NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM].freeze
|
|
23
|
+
# Segments allowed by POST /v2/forever/orders (create).
|
|
24
|
+
FOREVER_ORDER_ALL = [NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM].freeze
|
|
25
|
+
# Segments for conditional trigger (equities and indices only). POST/PUT /v2/alerts/orders.
|
|
26
|
+
ALERT_CONDITION_ALL = [NSE_EQ, BSE_EQ, IDX_I].freeze
|
|
27
|
+
# Segments allowed by POST /v2/charts/historical and POST /v2/charts/intraday (excludes NSE_COMM).
|
|
28
|
+
CHART_ALL = [IDX_I, NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM].freeze
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
# Product types for order placement.
|
|
@@ -30,6 +38,10 @@ module DhanHQ
|
|
|
30
38
|
BO = "BO"
|
|
31
39
|
|
|
32
40
|
ALL = [CNC, INTRADAY, MARGIN, MTF, CO, BO].freeze
|
|
41
|
+
# Product types allowed by POST /v2/margincalculator (single and multi).
|
|
42
|
+
MARGIN_CALC_ALL = [CNC, INTRADAY, MARGIN, MTF].freeze
|
|
43
|
+
# Product types allowed by POST /v2/forever/orders (create). Only CNC and MTF.
|
|
44
|
+
FOREVER_ORDER_ALL = [CNC, MTF].freeze
|
|
33
45
|
end
|
|
34
46
|
|
|
35
47
|
# Buy/Sell transaction types.
|
|
@@ -112,6 +124,17 @@ module DhanHQ
|
|
|
112
124
|
# Backward-compatible alias kept for existing SDK usage.
|
|
113
125
|
Instrument = InstrumentType
|
|
114
126
|
|
|
127
|
+
# Minute intervals allowed by POST /v2/charts/intraday (charts annexure).
|
|
128
|
+
module ChartInterval
|
|
129
|
+
ONE = "1"
|
|
130
|
+
FIVE = "5"
|
|
131
|
+
FIFTEEN = "15"
|
|
132
|
+
TWENTY_FIVE = "25"
|
|
133
|
+
SIXTY = "60"
|
|
134
|
+
|
|
135
|
+
ALL = [ONE, FIVE, FIFTEEN, TWENTY_FIVE, SIXTY].freeze
|
|
136
|
+
end
|
|
137
|
+
|
|
115
138
|
# Option types for derivatives trading.
|
|
116
139
|
module OptionType
|
|
117
140
|
CALL = "CALL"
|
|
@@ -365,10 +388,33 @@ module DhanHQ
|
|
|
365
388
|
ORDER_MODIFICATIONS_PER_ORDER = 25
|
|
366
389
|
end
|
|
367
390
|
|
|
391
|
+
# Canonical Dhan API, auth, WebSocket and instrument URLs (see https://dhanhq.co/docs/v2/).
|
|
392
|
+
# Configuration and Auth use these as defaults; ENV overrides apply at runtime.
|
|
393
|
+
module Urls
|
|
394
|
+
REST_API_BASE = "https://api.dhan.co/v2"
|
|
395
|
+
SANDBOX_API_BASE = "https://sandbox.dhan.co/v2"
|
|
396
|
+
AUTH_BASE = "https://auth.dhan.co"
|
|
397
|
+
WS_MARKET_FEED = "wss://api-feed.dhan.co"
|
|
398
|
+
WS_ORDER_UPDATE = "wss://api-order-update.dhan.co"
|
|
399
|
+
WS_DEPTH_20 = "wss://depth-api-feed.dhan.co/twentydepth"
|
|
400
|
+
WS_DEPTH_200 = "wss://full-depth-api.dhan.co/twohundreddepth"
|
|
401
|
+
INSTRUMENT_CSV_COMPACT = "https://images.dhan.co/api-data/api-scrip-master.csv"
|
|
402
|
+
INSTRUMENT_CSV_DETAILED = "https://images.dhan.co/api-data/api-scrip-master-detailed.csv"
|
|
403
|
+
DOCS = "https://dhanhq.co/docs/v2"
|
|
404
|
+
# Origin header value for WebSocket connections (Dhan main site).
|
|
405
|
+
ORIGIN = "https://dhanhq.co"
|
|
406
|
+
end
|
|
407
|
+
|
|
368
408
|
# Backward-compatible arrays used across existing validations.
|
|
369
409
|
TRANSACTION_TYPES = TransactionType::ALL
|
|
370
410
|
EXCHANGE_SEGMENTS = ExchangeSegment::ALL
|
|
411
|
+
CHART_EXCHANGE_SEGMENTS = ExchangeSegment::CHART_ALL
|
|
371
412
|
INSTRUMENTS = InstrumentType::ALL
|
|
413
|
+
CHART_INTERVALS = ChartInterval::ALL
|
|
414
|
+
MARGIN_CALCULATOR_SEGMENTS = ExchangeSegment::MARGIN_CALC_ALL
|
|
415
|
+
MARGIN_PRODUCT_TYPES = ProductType::MARGIN_CALC_ALL
|
|
416
|
+
FOREVER_ORDER_SEGMENTS = ExchangeSegment::FOREVER_ORDER_ALL
|
|
417
|
+
FOREVER_ORDER_PRODUCT_TYPES = ProductType::FOREVER_ORDER_ALL
|
|
372
418
|
PRODUCT_TYPES = ProductType::ALL
|
|
373
419
|
ORDER_TYPES = OrderType::ALL
|
|
374
420
|
VALIDITY_TYPES = Validity::ALL
|
|
@@ -376,6 +422,8 @@ module DhanHQ
|
|
|
376
422
|
ORDER_STATUSES = OrderStatus::ALL
|
|
377
423
|
COMPARISON_TYPES = ComparisonType::ALL
|
|
378
424
|
OPERATORS = Operator::ALL
|
|
425
|
+
ALERT_CONDITION_SEGMENTS = ExchangeSegment::ALERT_CONDITION_ALL
|
|
426
|
+
ALERT_TIMEFRAMES = %w[DATE ONE_MIN FIVE_MIN FIFTEEN_MIN DAY].freeze
|
|
379
427
|
|
|
380
428
|
# Exchange aliases used when building subscription payloads.
|
|
381
429
|
NSE = ExchangeSegment::NSE_EQ
|
|
@@ -387,7 +435,8 @@ module DhanHQ
|
|
|
387
435
|
BSE_FNO = ExchangeSegment::BSE_FNO
|
|
388
436
|
INDEX = ExchangeSegment::IDX_I
|
|
389
437
|
|
|
390
|
-
|
|
438
|
+
# Underlying segments accepted by POST /v2/optionchain and POST /v2/optionchain/expirylist.
|
|
439
|
+
OPTION_CHAIN_UNDERLYING_SEGMENTS = %w[IDX_I NSE_FNO BSE_FNO MCX_FO].freeze
|
|
391
440
|
|
|
392
441
|
# Canonical labels kept for compatibility with previous SDK versions.
|
|
393
442
|
BUY = TransactionType::BUY
|
|
@@ -409,15 +458,30 @@ module DhanHQ
|
|
|
409
458
|
IOC = Validity::IOC
|
|
410
459
|
|
|
411
460
|
# Download URL for the compact instrument master CSV.
|
|
412
|
-
COMPACT_CSV_URL =
|
|
461
|
+
COMPACT_CSV_URL = Urls::INSTRUMENT_CSV_COMPACT
|
|
413
462
|
# Download URL for the detailed instrument master CSV.
|
|
414
|
-
DETAILED_CSV_URL =
|
|
463
|
+
DETAILED_CSV_URL = Urls::INSTRUMENT_CSV_DETAILED
|
|
415
464
|
|
|
416
465
|
# API route prefixes that require a `client-id` header in addition to the access token.
|
|
417
466
|
DATA_API_PREFIXES = [
|
|
418
467
|
"/v2/marketfeed/",
|
|
419
468
|
"/v2/optionchain",
|
|
420
|
-
"/v2/instrument/"
|
|
469
|
+
"/v2/instrument/",
|
|
470
|
+
"/v2/charts"
|
|
471
|
+
].freeze
|
|
472
|
+
|
|
473
|
+
# Path prefixes for which the request body (POST/PUT/PATCH) must include dhanClientId.
|
|
474
|
+
# Injection is done in the client layer when building the payload.
|
|
475
|
+
PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES = %w[
|
|
476
|
+
/alerts/orders
|
|
477
|
+
/v2/orders
|
|
478
|
+
/v2/forever
|
|
479
|
+
/v2/super/orders
|
|
480
|
+
/v2/positions
|
|
481
|
+
/v2/pnlExit
|
|
482
|
+
/v2/margincalculator
|
|
483
|
+
/v2/killswitch
|
|
484
|
+
/v2/ip
|
|
421
485
|
].freeze
|
|
422
486
|
|
|
423
487
|
# Mapping of exchange and segment combinations to canonical exchange segment names.
|
|
@@ -2,45 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates alert order payloads for
|
|
6
|
-
# Condition
|
|
5
|
+
# Validates Conditional Trigger (alert order) payloads for POST /v2/alerts/orders and PUT /v2/alerts/orders/{alertId}.
|
|
6
|
+
# Condition: exchangeSegment (NSE_EQ|BSE_EQ|IDX_I), timeframe (required), comparisonType, operator, expDate, frequency;
|
|
7
|
+
# indicatorName/time_frame required for TECHNICAL_* comparison types.
|
|
8
|
+
# Orders: transactionType, exchangeSegment, productType (CNC|INTRADAY|MARGIN|MTF), orderType, securityId,
|
|
9
|
+
# quantity, validity, price (required); discQuantity, triggerPrice optional.
|
|
7
10
|
class AlertOrderContract < BaseContract
|
|
8
11
|
params do
|
|
9
12
|
required(:condition).hash do
|
|
10
13
|
required(:security_id).filled(:string, max_size?: 20)
|
|
11
|
-
required(:exchange_segment).filled(:string, included_in?:
|
|
14
|
+
required(:exchange_segment).filled(:string, included_in?: ALERT_CONDITION_SEGMENTS)
|
|
12
15
|
required(:comparison_type).filled(:string, included_in?: COMPARISON_TYPES)
|
|
13
|
-
|
|
14
|
-
optional(:time_frame).maybe(:string)
|
|
16
|
+
required(:time_frame).filled(:string, included_in?: ALERT_TIMEFRAMES)
|
|
15
17
|
required(:operator).filled(:string, included_in?: OPERATORS)
|
|
18
|
+
required(:exp_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}\z/)
|
|
19
|
+
required(:frequency).filled(:string)
|
|
20
|
+
optional(:indicator_name).maybe(:string)
|
|
16
21
|
optional(:comparing_value).maybe(:float)
|
|
17
22
|
optional(:comparing_indicator_name).maybe(:string)
|
|
18
|
-
|
|
19
|
-
required(:frequency).filled(:string)
|
|
23
|
+
optional(:user_note).maybe(:string)
|
|
20
24
|
end
|
|
21
25
|
required(:orders).array(:hash) do
|
|
22
26
|
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
23
27
|
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
24
|
-
required(:product_type).filled(:string, included_in?:
|
|
28
|
+
required(:product_type).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
|
|
25
29
|
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
26
30
|
required(:security_id).filled(:string, max_size?: 20)
|
|
27
31
|
required(:quantity).filled(:integer, gt?: 0)
|
|
28
32
|
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
29
|
-
|
|
30
|
-
optional(:
|
|
33
|
+
required(:price).filled # string or number; API expects string, coerce in serialization
|
|
34
|
+
optional(:disc_quantity).maybe(:string)
|
|
35
|
+
optional(:trigger_price).maybe(:string)
|
|
31
36
|
end
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
rule(condition: :indicator_name) do
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
next unless values.dig(:condition, :comparison_type).to_s.start_with?("TECHNICAL")
|
|
41
|
+
next if value && !value.to_s.strip.empty?
|
|
42
|
+
|
|
43
|
+
key(%i[condition indicator_name]).failure("is required for technical comparisons")
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
rule(condition: :time_frame) do
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
next unless values.dig(:condition, :comparison_type).to_s.start_with?("TECHNICAL")
|
|
48
|
+
next if value && !value.to_s.strip.empty?
|
|
49
|
+
|
|
50
|
+
key(%i[condition time_frame]).failure("is required for technical comparisons")
|
|
44
51
|
end
|
|
45
52
|
end
|
|
46
53
|
end
|
|
@@ -23,7 +23,9 @@ module DhanHQ
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
rule(:exchange_segment) do
|
|
26
|
-
|
|
26
|
+
# IDX_I for index options, NSE_EQ/BSE_EQ for equity options,
|
|
27
|
+
# plus NSE_FNO/BSE_FNO for derivatives.
|
|
28
|
+
valid_segments = %w[IDX_I NSE_EQ BSE_EQ NSE_FNO BSE_FNO]
|
|
27
29
|
key.failure("must be one of: #{valid_segments.join(", ")}") unless valid_segments.include?(value)
|
|
28
30
|
end
|
|
29
31
|
|
|
@@ -43,7 +45,7 @@ module DhanHQ
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
rule(:strike) do
|
|
46
|
-
unless value.match?(/\AATM(\+|-)?\d
|
|
48
|
+
unless value.match?(/\AATM(\+|-)?\d+\z/) || value == "ATM"
|
|
47
49
|
key.failure("must be in format ATM, ATM+1, ATM-1, etc. " \
|
|
48
50
|
"(up to ATM+10/ATM-10 for Index Options, ATM+3/ATM-3 for others)")
|
|
49
51
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/forever/orders (create Forever / GTT order).
|
|
6
|
+
# orderFlag: SINGLE | OCO. productType: CNC | MTF. For OCO, price1, triggerPrice1, quantity1 required.
|
|
7
|
+
class ForeverOrderCreateContract < BaseContract
|
|
8
|
+
params do
|
|
9
|
+
required(:dhan_client_id).filled(:string)
|
|
10
|
+
required(:order_flag).filled(:string, included_in?: OrderFlag::ALL)
|
|
11
|
+
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
12
|
+
required(:exchange_segment).filled(:string, included_in?: FOREVER_ORDER_SEGMENTS)
|
|
13
|
+
required(:product_type).filled(:string, included_in?: FOREVER_ORDER_PRODUCT_TYPES)
|
|
14
|
+
required(:order_type).filled(:string, included_in?: %w[LIMIT MARKET])
|
|
15
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
16
|
+
required(:security_id).filled(:string)
|
|
17
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
18
|
+
required(:price).filled(:float, gt?: 0)
|
|
19
|
+
required(:trigger_price).filled(:float)
|
|
20
|
+
optional(:correlation_id).maybe(:string, max_size?: 30, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
21
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
22
|
+
optional(:price1).maybe(:float, gt?: 0)
|
|
23
|
+
optional(:trigger_price1).maybe(:float)
|
|
24
|
+
optional(:quantity1).maybe(:integer, gt?: 0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule(:order_flag) do
|
|
28
|
+
next unless value == DhanHQ::Constants::OrderFlag::OCO
|
|
29
|
+
|
|
30
|
+
missing = []
|
|
31
|
+
missing << "price1" if values[:price1].nil? || values[:price1].to_f <= 0
|
|
32
|
+
missing << "trigger_price1" if values[:trigger_price1].nil?
|
|
33
|
+
missing << "quantity1" if values[:quantity1].nil? || values[:quantity1].to_i < 1
|
|
34
|
+
key.failure("required for OCO: #{missing.join(", ")}") if missing.any?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validates request for PUT /v2/forever/orders/{order-id} (modify Forever order).
|
|
39
|
+
# orderType: LIMIT | MARKET | STOP_LOSS | STOP_LOSS_MARKET. legName: TARGET_LEG | STOP_LOSS_LEG.
|
|
40
|
+
class ForeverOrderModifyContract < BaseContract
|
|
41
|
+
params do
|
|
42
|
+
required(:dhan_client_id).filled(:string)
|
|
43
|
+
required(:order_id).filled(:string)
|
|
44
|
+
required(:order_flag).filled(:string, included_in?: OrderFlag::ALL)
|
|
45
|
+
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
46
|
+
required(:leg_name).filled(:string, included_in?: %w[TARGET_LEG STOP_LOSS_LEG])
|
|
47
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
48
|
+
required(:price).filled(:float, gt?: 0)
|
|
49
|
+
required(:trigger_price).filled(:float)
|
|
50
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
51
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -4,40 +4,38 @@ require "date"
|
|
|
4
4
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
module Contracts
|
|
7
|
-
# Validates payloads for
|
|
7
|
+
# Validates payloads for POST /v2/charts/historical (daily OHLC). No interval.
|
|
8
8
|
class HistoricalDataContract < BaseContract
|
|
9
9
|
params do
|
|
10
|
-
# Common required fields
|
|
11
10
|
required(:security_id).filled(:string)
|
|
12
|
-
required(:exchange_segment).filled(:string, included_in?:
|
|
11
|
+
required(:exchange_segment).filled(:string, included_in?: CHART_EXCHANGE_SEGMENTS)
|
|
13
12
|
required(:instrument).filled(:string, included_in?: INSTRUMENTS)
|
|
13
|
+
required(:from_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
14
|
+
required(:to_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Optional fields
|
|
20
|
-
optional(:expiry_code).maybe(:integer, included_in?: [0, 1, 2])
|
|
21
|
-
|
|
22
|
-
# For intraday, the user can supply an "interval"
|
|
23
|
-
# (valid: 1, 5, 15, 25, 60)
|
|
24
|
-
optional(:interval).maybe(:string, included_in?: %w[1 5 15 25 60])
|
|
16
|
+
optional(:expiry_code).maybe(:integer, included_in?: ExpiryCode::ALL)
|
|
17
|
+
optional(:interval).maybe(:string, included_in?: CHART_INTERVALS)
|
|
18
|
+
optional(:oi).maybe(:bool)
|
|
25
19
|
end
|
|
26
20
|
|
|
27
21
|
rule(:from_date) do
|
|
28
|
-
next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
22
|
+
next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
# Only validate weekend for pure date strings (YYYY-MM-DD)
|
|
25
|
+
if value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
26
|
+
d = Date.parse(value)
|
|
27
|
+
key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(d)
|
|
28
|
+
end
|
|
32
29
|
rescue Date::Error
|
|
33
30
|
key.failure("invalid date format")
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
rule(:from_date, :to_date) do
|
|
37
|
-
next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}
|
|
34
|
+
next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/) &&
|
|
35
|
+
values[:to_date].match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
38
36
|
|
|
39
|
-
from_date =
|
|
40
|
-
to_date =
|
|
37
|
+
from_date = DateTime.parse(values[:from_date])
|
|
38
|
+
to_date = DateTime.parse(values[:to_date])
|
|
41
39
|
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
42
40
|
rescue Date::Error
|
|
43
41
|
key.failure("invalid date format")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates payloads for POST /v2/charts/intraday (minute OHLC). Requires interval.
|
|
6
|
+
class IntradayHistoricalDataContract < HistoricalDataContract
|
|
7
|
+
params do
|
|
8
|
+
required(:interval).filled(:string, included_in?: CHART_INTERVALS)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,39 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates
|
|
6
|
-
|
|
5
|
+
# Validates request for POST /v2/margincalculator (single order).
|
|
6
|
+
# dhanClientId, exchangeSegment (NSE_EQ|NSE_FNO|BSE_EQ|BSE_FNO|MCX_COMM), transactionType, quantity,
|
|
7
|
+
# productType (CNC|INTRADAY|MARGIN|MTF), securityId, price (required);
|
|
8
|
+
# orderType (optional, but required by some accounts); triggerPrice (optional, for SL-M/SL-L).
|
|
9
|
+
class MarginCalculatorContract < BaseContract
|
|
7
10
|
params do
|
|
8
11
|
required(:dhanClientId).filled(:string)
|
|
9
|
-
required(:exchangeSegment).filled(:string, included_in?:
|
|
10
|
-
required(:transactionType).filled(:string, included_in?:
|
|
12
|
+
required(:exchangeSegment).filled(:string, included_in?: MARGIN_CALCULATOR_SEGMENTS)
|
|
13
|
+
required(:transactionType).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
11
14
|
required(:quantity).filled(:integer, gt?: 0)
|
|
12
|
-
required(:productType).filled(:string, included_in?:
|
|
15
|
+
required(:productType).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
|
|
16
|
+
optional(:orderType).maybe(:string, included_in?: ORDER_TYPES)
|
|
13
17
|
required(:securityId).filled(:string)
|
|
14
|
-
|
|
18
|
+
required(:price).filled(:float, gt?: 0)
|
|
15
19
|
optional(:triggerPrice).maybe(:float)
|
|
16
20
|
end
|
|
21
|
+
|
|
17
22
|
rule(:price) do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
elsif values[:price].is_a?(Float) && (values[:price].nan? || values[:price].infinite?)
|
|
22
|
-
key(:price).failure("must be a finite number")
|
|
23
|
-
end
|
|
24
|
-
end
|
|
23
|
+
next unless values[:price].is_a?(Float)
|
|
24
|
+
|
|
25
|
+
key.failure("must be a finite number") if values[:price].nan? || values[:price].infinite?
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
rule(:triggerPrice) do
|
|
28
|
-
|
|
29
|
+
next unless values[:triggerPrice].is_a?(Float)
|
|
30
|
+
|
|
31
|
+
key.failure("must be a finite number") if values[:triggerPrice].nan? || values[:triggerPrice].infinite?
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
# Segment-Based Product Restrictions for margin calculations
|
|
32
34
|
rule(:productType, :exchangeSegment) do
|
|
33
35
|
case values[:productType]
|
|
34
36
|
when DhanHQ::Constants::ProductType::CNC, DhanHQ::Constants::ProductType::MTF
|
|
35
|
-
key
|
|
37
|
+
key.failure("is only allowed for Equity segments (NSE_EQ, BSE_EQ)") unless values[:exchangeSegment].to_s.end_with?("_EQ")
|
|
36
38
|
when DhanHQ::Constants::ProductType::MARGIN
|
|
37
|
-
key
|
|
39
|
+
key.failure("is not allowed for Equity Cash segments; use CNC or INTRADAY") if values[:exchangeSegment].to_s.end_with?("_EQ")
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request payloads for Market Feed endpoints (LTP, OHLC, Quote).
|
|
6
|
+
#
|
|
7
|
+
# The Market Feed API expects a payload where keys are Exchange Segments
|
|
8
|
+
# and values are Arrays of security IDs (Integers).
|
|
9
|
+
#
|
|
10
|
+
# @example Valid payload:
|
|
11
|
+
# {
|
|
12
|
+
# "NSE_EQ": [11536, 3456],
|
|
13
|
+
# "NSE_FNO": [49081, 49082]
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
class MarketFeedContract < BaseContract
|
|
17
|
+
params do
|
|
18
|
+
config.validate_keys = true
|
|
19
|
+
|
|
20
|
+
# Dynamically define all valid exchange segments as optional keys.
|
|
21
|
+
# Each must be an array of integers.
|
|
22
|
+
EXCHANGE_SEGMENTS.each do |segment|
|
|
23
|
+
optional(segment.to_sym).array(:integer)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule do
|
|
28
|
+
base.failure("must provide at least one exchange segment and security ID") if values.to_h.empty?
|
|
29
|
+
|
|
30
|
+
total_instruments = 0
|
|
31
|
+
values.to_h.each do |key, value|
|
|
32
|
+
if value.is_a?(Array)
|
|
33
|
+
key(key).failure("must not be empty") if value.empty?
|
|
34
|
+
total_instruments += value.size
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
base.failure("cannot fetch more than 1000 instruments in a single request (found #{total_instruments})") if total_instruments > 1000
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates
|
|
5
|
+
# Validates request for POST /v2/margincalculator/multi.
|
|
6
|
+
# Top-level: includePosition, includeOrder, dhanClientId, scripList.
|
|
7
|
+
# Each scrip: exchangeSegment, transactionType, quantity, productType, securityId, price; triggerPrice optional.
|
|
6
8
|
class MultiScripMarginCalcRequestContract < BaseContract
|
|
7
9
|
params do
|
|
8
10
|
optional(:dhanClientId).maybe(:string)
|
|
9
11
|
optional(:includePosition).maybe(:bool)
|
|
10
12
|
optional(:includeOrder).maybe(:bool)
|
|
11
13
|
required(:scripList).array(:hash) do
|
|
12
|
-
required(:exchangeSegment).filled(:string, included_in?:
|
|
14
|
+
required(:exchangeSegment).filled(:string, included_in?: MARGIN_CALCULATOR_SEGMENTS)
|
|
13
15
|
required(:transactionType).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
14
16
|
required(:quantity).filled(:integer, gt?: 0)
|
|
15
|
-
required(:productType).filled(:string, included_in?:
|
|
17
|
+
required(:productType).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
|
|
18
|
+
optional(:orderType).maybe(:string, included_in?: ORDER_TYPES)
|
|
16
19
|
required(:securityId).filled(:string)
|
|
17
|
-
|
|
18
|
-
optional(:triggerPrice).maybe(:float
|
|
20
|
+
required(:price).filled(:float, gt?: 0)
|
|
21
|
+
optional(:triggerPrice).maybe(:float)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "date"
|
|
4
4
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
module Contracts
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Validates request parameters for fetching option chains.
|
|
7
|
+
# Validates request for POST /v2/optionchain (option chain by underlying and expiry).
|
|
8
|
+
# UnderlyingScrip (int), UnderlyingSeg (enum), Expiry (YYYY-MM-DD). Rate limit: 1 request per 3 seconds.
|
|
10
9
|
class OptionChainContract < BaseContract
|
|
11
10
|
params do
|
|
12
|
-
required(:underlying_scrip).filled(:integer)
|
|
13
|
-
required(:underlying_seg).filled(:string, included_in?:
|
|
11
|
+
required(:underlying_scrip).filled(:integer)
|
|
12
|
+
required(:underlying_seg).filled(:string, included_in?: OPTION_CHAIN_UNDERLYING_SEGMENTS)
|
|
14
13
|
required(:expiry).filled(:string)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
rule(:expiry) do
|
|
18
|
-
|
|
19
|
-
key.failure("must be in 'YYYY-MM-DD' format") unless value.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
17
|
+
next unless value.is_a?(String)
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
key.failure("must be a valid date") unless parsed_date.to_s == value
|
|
25
|
-
rescue ArgumentError
|
|
26
|
-
key.failure("is not a valid date")
|
|
19
|
+
unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
20
|
+
key.failure("must be in YYYY-MM-DD format")
|
|
21
|
+
next
|
|
27
22
|
end
|
|
23
|
+
|
|
24
|
+
Date.parse(value)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
key.failure("must be a valid date")
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Validates request parameters for fetching expiry lists (expiry not required).
|
|
30
|
+
# Validates request for POST /v2/optionchain/expirylist (expiry list for an underlying).
|
|
31
|
+
# UnderlyingScrip (int), UnderlyingSeg (enum). No Expiry.
|
|
34
32
|
class OptionChainExpiryListContract < BaseContract
|
|
35
33
|
params do
|
|
36
|
-
required(:underlying_scrip).filled(:integer)
|
|
37
|
-
required(:underlying_seg).filled(:string, included_in?:
|
|
34
|
+
required(:underlying_scrip).filled(:integer)
|
|
35
|
+
required(:underlying_seg).filled(:string, included_in?: OPTION_CHAIN_UNDERLYING_SEGMENTS)
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
end
|
|
@@ -13,7 +13,7 @@ module DhanHQ
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
rule(:profitValue, :lossValue) do
|
|
16
|
-
key.failure("at least one of profitValue or lossValue must be provided")
|
|
16
|
+
key.failure("at least one of profitValue or lossValue must be provided") unless values[:profitValue] || values[:lossValue]
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|