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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -3
  3. data/ARCHITECTURE.md +113 -0
  4. data/CHANGELOG.md +55 -0
  5. data/README.md +2 -0
  6. data/Rakefile +3 -1
  7. data/docs/API_VERIFICATION.md +10 -8
  8. data/docs/ENDPOINTS_AND_SANDBOX.md +115 -0
  9. data/lib/DhanHQ/auth.rb +2 -2
  10. data/lib/DhanHQ/client.rb +72 -51
  11. data/lib/DhanHQ/configuration.rb +45 -11
  12. data/lib/DhanHQ/constants.rb +68 -4
  13. data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
  14. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
  15. data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
  16. data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
  17. data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
  18. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
  19. data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
  20. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
  21. data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
  22. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
  23. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
  24. data/lib/DhanHQ/core/auth_api.rb +1 -1
  25. data/lib/DhanHQ/core/base_api.rb +10 -9
  26. data/lib/DhanHQ/core/base_model.rb +4 -1
  27. data/lib/DhanHQ/core/error_handler.rb +2 -2
  28. data/lib/DhanHQ/errors.rb +14 -2
  29. data/lib/DhanHQ/helpers/request_helper.rb +27 -5
  30. data/lib/DhanHQ/helpers/response_helper.rb +48 -19
  31. data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
  32. data/lib/DhanHQ/models/alert_order.rb +6 -2
  33. data/lib/DhanHQ/models/edis.rb +20 -13
  34. data/lib/DhanHQ/models/expired_options_data.rb +54 -44
  35. data/lib/DhanHQ/models/forever_order.rb +17 -7
  36. data/lib/DhanHQ/models/historical_data.rb +40 -6
  37. data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
  38. data/lib/DhanHQ/models/margin.rb +62 -82
  39. data/lib/DhanHQ/models/market_feed.rb +14 -3
  40. data/lib/DhanHQ/models/option_chain.rb +50 -150
  41. data/lib/DhanHQ/models/order.rb +19 -4
  42. data/lib/DhanHQ/models/super_order.rb +2 -2
  43. data/lib/DhanHQ/resources/alert_orders.rb +1 -1
  44. data/lib/DhanHQ/resources/edis.rb +4 -3
  45. data/lib/DhanHQ/resources/instruments.rb +3 -2
  46. data/lib/DhanHQ/resources/ip_setup.rb +4 -1
  47. data/lib/DhanHQ/resources/kill_switch.rb +7 -1
  48. data/lib/DhanHQ/resources/orders.rb +1 -1
  49. data/lib/DhanHQ/resources/super_orders.rb +8 -2
  50. data/lib/DhanHQ/resources/trader_control.rb +13 -4
  51. data/lib/DhanHQ/version.rb +1 -1
  52. data/lib/DhanHQ/ws/base_connection.rb +1 -1
  53. data/lib/DhanHQ/ws/client.rb +2 -1
  54. data/lib/DhanHQ/ws/market_depth/client.rb +16 -8
  55. data/lib/dhan_hq.rb +37 -32
  56. data/lib/ta/indicators.rb +15 -18
  57. metadata +7 -9
  58. data/CODE_REVIEW_ISSUES.md +0 -397
  59. data/FIXES_APPLIED.md +0 -373
  60. data/RELEASING.md +0 -60
  61. data/REVIEW_SUMMARY.md +0 -120
  62. data/VERSION_UPDATE.md +0 -82
  63. data/diagram.md +0 -34
  64. data/docs/ARCHIVE_README.md +0 -784
@@ -31,7 +31,7 @@ module DhanHQ
31
31
  # Parameters and validation rules for the slicing order request.
32
32
  #
33
33
  # @!attribute [r] correlationId
34
- # @return [String] Optional. Identifier for tracking, max length 25 characters.
34
+ # @return [String] Optional. Identifier for tracking, max length 30 characters (per orders doc).
35
35
  # @!attribute [r] transactionType
36
36
  # @return [String] Required. BUY or SELL.
37
37
  # @!attribute [r] exchangeSegment
@@ -39,13 +39,13 @@ module DhanHQ
39
39
  # Must be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM.
40
40
  # @!attribute [r] productType
41
41
  # @return [String] Required. Product type for the order.
42
- # Must be one of: CNC, INTRADAY, MARGIN, MTF, CO, BO.
42
+ # Must be one of: CNC, INTRADAY, MARGIN, MTF (per orders doc).
43
43
  # @!attribute [r] orderType
44
44
  # @return [String] Required. Type of order.
45
45
  # Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
46
46
  # @!attribute [r] validity
47
47
  # @return [String] Required. Validity of the order.
48
- # Must be one of: DAY, IOC, GTC, GTD.
48
+ # Must be one of: DAY, IOC (per orders doc).
49
49
  # @!attribute [r] securityId
50
50
  # @return [String] Required. Security identifier for the order.
51
51
  # @!attribute [r] quantity
@@ -59,11 +59,11 @@ module DhanHQ
59
59
  # @!attribute [r] afterMarketOrder
60
60
  # @return [Boolean] Optional. Indicates if this is an after-market order.
61
61
  # @!attribute [r] amoTime
62
- # @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
62
+ # @return [String] Optional. Time for after-market orders. Must be one of: PRE_OPEN, OPEN, OPEN_30, OPEN_60.
63
63
  # @!attribute [r] boProfitValue
64
- # @return [Float] Optional. Profit value for Bracket Orders, must be > 0 if provided.
64
+ # @return [Float] Optional. Profit value for Bracket Orders (not used when productType is CNC/INTRADAY/MARGIN/MTF).
65
65
  # @!attribute [r] boStopLossValue
66
- # @return [Float] Optional. Stop-loss value for Bracket Orders, must be > 0 if provided.
66
+ # @return [Float] Optional. Stop-loss value for Bracket Orders (not used when productType is CNC/INTRADAY/MARGIN/MTF).
67
67
  # @!attribute [r] drvExpiryDate
68
68
  # @return [String] Optional. Expiry date for derivative contracts.
69
69
  # @!attribute [r] drvOptionType
@@ -71,21 +71,21 @@ module DhanHQ
71
71
  # @!attribute [r] drvStrikePrice
72
72
  # @return [Float] Optional. Strike price for options, must be > 0 if provided.
73
73
  params do
74
- optional(:correlationId).maybe(:string, max_size?: 25)
74
+ optional(:correlationId).maybe(:string, max_size?: 30)
75
75
  required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
76
76
  required(:exchangeSegment).filled(:string,
77
77
  included_in?: %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ BSE_FNO BSE_CURRENCY
78
78
  MCX_COMM])
79
- required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF CO BO])
79
+ required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF])
80
80
  required(:orderType).filled(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
81
- required(:validity).filled(:string, included_in?: %w[DAY IOC GTC GTD])
81
+ required(:validity).filled(:string, included_in?: %w[DAY IOC])
82
82
  required(:securityId).filled(:string)
83
83
  required(:quantity).filled(:integer, gt?: 0)
84
84
  optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
85
85
  optional(:price).maybe(:float, gt?: 0)
86
86
  optional(:triggerPrice).maybe(:float, gt?: 0)
87
87
  optional(:afterMarketOrder).maybe(:bool)
88
- optional(:amoTime).maybe(:string, included_in?: %w[OPEN OPEN_30 OPEN_60])
88
+ optional(:amoTime).maybe(:string, included_in?: %w[PRE_OPEN OPEN OPEN_30 OPEN_60])
89
89
  optional(:boProfitValue).maybe(:float, gt?: 0)
90
90
  optional(:boStopLossValue).maybe(:float, gt?: 0)
91
91
  optional(:drvExpiryDate).maybe(:string)
@@ -8,7 +8,7 @@ module DhanHQ
8
8
  # This class intentionally lives at the top-level namespace so it autoloads
9
9
  # cleanly from `lib/DhanHQ/core/auth_api.rb` with Zeitwerk `collapse`.
10
10
  class AuthAPI
11
- BASE_URL = "https://auth.dhan.co"
11
+ BASE_URL = Constants::Urls::AUTH_BASE
12
12
 
13
13
  def connection
14
14
  @connection ||= Faraday.new(url: BASE_URL) do |faraday|
@@ -86,20 +86,21 @@ module DhanHQ
86
86
  "#{self.class::HTTP_PATH}#{endpoint}"
87
87
  end
88
88
 
89
- # Format parameters based on API endpoint
89
+ # Format parameters based on API endpoint. Uses path-based strategy (marketfeed: pass-through,
90
+ # optionchain: titleize, default: camelize).
90
91
  def format_params(endpoint, params)
91
- return params if marketfeed_api?(endpoint) || params.empty?
92
+ full_path = build_path(endpoint)
93
+ return params if params.empty?
92
94
 
93
- optionchain_api?(endpoint) ? titleize_keys(params) : camelize_keys(params)
95
+ param_formatter_for(full_path).call(params)
94
96
  end
95
97
 
96
- # Determines if the API endpoint is for Option Chain
97
- def optionchain_api?(endpoint)
98
- endpoint.include?("/optionchain")
99
- end
98
+ # Returns a callable that formats params for the given path (Strategy).
99
+ def param_formatter_for(full_path)
100
+ return ->(p) { p } if full_path.include?("/marketfeed")
101
+ return ->(p) { titleize_keys(p) } if full_path.include?("/optionchain")
100
102
 
101
- def marketfeed_api?(endpoint)
102
- endpoint.include?("/marketfeed")
103
+ ->(p) { camelize_keys(p) }
103
104
  end
104
105
  end
105
106
  end
@@ -145,7 +145,10 @@ module DhanHQ
145
145
  def parse_collection_response(response)
146
146
  # Some endpoints return arrays, others might return a `[:data]` structure
147
147
  unless response.is_a?(Array) || (response.is_a?(Hash) && response[:data].is_a?(Array))
148
- DhanHQ.logger&.warn("[DhanHQ::BaseModel] Unexpected response format for collection: #{response.class}. Expected Array or Hash with :data key.")
148
+ DhanHQ.logger&.warn(
149
+ "[DhanHQ::BaseModel] Unexpected response format for collection: #{response.class}. " \
150
+ "Expected Array or Hash with :data key."
151
+ )
149
152
  return []
150
153
  end
151
154
 
@@ -10,9 +10,9 @@ module DhanHQ
10
10
  def self.handle(error)
11
11
  case error
12
12
  when Dry::Validation::Result
13
- raise "Validation Error: #{error.errors.to_h}"
13
+ raise DhanHQ::ValidationError, "Invalid parameters: #{error.errors.to_h}"
14
14
  else
15
- raise "Error: #{error.message}"
15
+ raise DhanHQ::Error, error.message
16
16
  end
17
17
  end
18
18
  end
data/lib/DhanHQ/errors.rb CHANGED
@@ -1,8 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DhanHQ
4
- # Base error class for all DhanHQ API errors
5
- class Error < StandardError; end
4
+ # Base error class for all DhanHQ API errors.
5
+ # When raised from API response handling, {#response_body} holds the parsed error payload.
6
+ class Error < StandardError
7
+ attr_reader :response_body
8
+
9
+ def initialize(message = nil, response_body: nil)
10
+ super(message)
11
+ @response_body = response_body
12
+ end
13
+ end
6
14
 
7
15
  # Authentication and access errors
8
16
  # Raised when access token cannot be resolved (missing config or provider returned nil).
@@ -36,6 +44,10 @@ module DhanHQ
36
44
 
37
45
  # Order and market data errors
38
46
  class OrderError < Error; end
47
+
48
+ # Raised when the 25-modifications-per-order API cap would be exceeded.
49
+ # Count is tracked per Order instance in this process only (see Order#modify).
50
+ class ModificationLimitError < Error; end
39
51
  # Raised when the API signals an issue with the requested data payload.
40
52
  class DataError < Error; end
41
53
 
@@ -39,7 +39,7 @@ module DhanHQ
39
39
  "access-token" => token
40
40
  }
41
41
 
42
- # Add client-id for DATA APIs
42
+ # Add client-id for DATA APIs (now including sandbox profile/funds)
43
43
  if data_api?(path)
44
44
  client_id = DhanHQ.configuration&.client_id
45
45
  unless client_id
@@ -70,19 +70,41 @@ module DhanHQ
70
70
  # @param req [Faraday::Request] The request object.
71
71
  # @param payload [Hash] The request payload.
72
72
  # @param method [Symbol] The HTTP method.
73
- def prepare_payload(req, payload, method)
74
- return if payload.nil? || payload.empty?
73
+ def prepare_payload(req, payload, method, path = nil)
74
+ return if payload.nil? || (payload.empty? && (path.nil? || !data_api?(path)))
75
75
 
76
76
  unless payload.is_a?(Hash)
77
77
  raise DhanHQ::InputExceptionError,
78
78
  "Invalid payload: Expected a Hash, got #{payload.class}"
79
79
  end
80
80
 
81
+ out = payload
82
+ if path && %i[post put patch].include?(method)
83
+ client_id = DhanHQ.configuration&.client_id
84
+ needs_client_id = data_api?(path) || payload_requires_dhan_client_id?(path)
85
+ if client_id && needs_client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
86
+ out = payload.dup
87
+ if out.keys.any?(String)
88
+ out["dhanClientId"] = client_id
89
+ else
90
+ out[:dhanClientId] = client_id
91
+ end
92
+ end
93
+ end
94
+
81
95
  case method
82
96
  when :delete then req.params = {}
83
- when :get then req.params = payload
84
- else req.body = payload.to_json
97
+ when :get then req.params = out
98
+ else req.body = out.to_json
85
99
  end
86
100
  end
101
+
102
+ # True when the path is one where the request body must include dhanClientId (order-api style).
103
+ def payload_requires_dhan_client_id?(path)
104
+ return false if path.nil? || path.empty?
105
+
106
+ prefixes = DhanHQ::Constants::PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES
107
+ prefixes.any? { |p| path.start_with?(p) }
108
+ end
87
109
  end
88
110
  end
@@ -3,6 +3,14 @@
3
3
  module DhanHQ
4
4
  # Helper mixin for normalising API responses and raising mapped errors.
5
5
  module ResponseHelper
6
+ STATUS_ERROR_FALLBACK = {
7
+ 400 => DhanHQ::InputExceptionError,
8
+ 401 => DhanHQ::InvalidAuthenticationError,
9
+ 403 => DhanHQ::InvalidAccessError,
10
+ 404 => DhanHQ::NotFoundError,
11
+ 429 => DhanHQ::RateLimitError
12
+ }.freeze
13
+
6
14
  private
7
15
 
8
16
  # Determines if the API response indicates success.
@@ -53,31 +61,52 @@ module DhanHQ
53
61
  end
54
62
 
55
63
  error_class = DhanHQ::Constants::DHAN_ERROR_MAPPING[error_code]
56
-
57
64
  unless error_class
58
- # Log unmapped error codes for investigation
59
65
  DhanHQ.logger&.warn("[DhanHQ] Unmapped error code: #{error_code} (status: #{response.status})")
66
+ error_class = status_fallback_error_class(response.status)
67
+ end
60
68
 
61
- error_class =
62
- case response.status
63
- when 400 then DhanHQ::InputExceptionError
64
- when 401 then DhanHQ::InvalidAuthenticationError
65
- when 403 then DhanHQ::InvalidAccessError
66
- when 404 then DhanHQ::NotFoundError
67
- when 429 then DhanHQ::RateLimitError
68
- when 500..599 then DhanHQ::InternalServerError
69
- else DhanHQ::OtherError
70
- end
69
+ message = build_error_text(error_code, error_message, body)
70
+ raise error_class.new(message, response_body: body)
71
+ end
72
+
73
+ def status_fallback_error_class(status)
74
+ STATUS_ERROR_FALLBACK[status] ||
75
+ (status.between?(500, 599) ? DhanHQ::InternalServerError : DhanHQ::OtherError)
76
+ end
77
+
78
+ def build_error_text(error_code, error_message, body = {})
79
+ text = if error_code == DhanHQ::Constants::TradingErrorCode::NO_HOLDINGS
80
+ "#{error_message} (error code: #{error_code})"
81
+ else
82
+ "#{error_code}: #{error_message}"
83
+ end
84
+
85
+ extra = extra_error_detail(body)
86
+ text += " | #{extra}" if extra
87
+
88
+ if error_code == DhanHQ::Constants::TradingErrorCode::INPUT_EXCEPTION
89
+ text += " (API does not return which field failed; check required params and value types for this endpoint.)"
71
90
  end
72
91
 
73
- error_text =
74
- if error_code == DhanHQ::Constants::TradingErrorCode::NO_HOLDINGS
75
- "#{error_message} (error code: #{error_code})"
76
- else
77
- "#{error_code}: #{error_message}"
78
- end
92
+ text
93
+ end
79
94
 
80
- raise error_class, error_text
95
+ # Returns any additional error detail from the response body (errors array, details, etc.).
96
+ def extra_error_detail(body)
97
+ return nil unless body.is_a?(Hash)
98
+
99
+ parts = []
100
+ if body[:errors].is_a?(Array) && body[:errors].any?
101
+ parts << body[:errors].join("; ")
102
+ end
103
+ if body[:details].is_a?(String) && body[:details].to_s.strip != ""
104
+ parts << body[:details].to_s
105
+ end
106
+ if body[:validationErrors].is_a?(Array) && body[:validationErrors].any?
107
+ parts << body[:validationErrors].map { |e| e.is_a?(Hash) ? e[:message] || e[:field] : e }.join("; ")
108
+ end
109
+ parts.empty? ? nil : parts.join(" ")
81
110
  end
82
111
 
83
112
  # Parses JSON response safely. Converts response body to a hash or array with indifferent access.
@@ -11,7 +11,9 @@ module DhanHQ
11
11
  contract = contract_class.new
12
12
  result = contract.call(params)
13
13
 
14
- raise DhanHQ::Error, "Validation Error: #{result.errors.to_h}" unless result.success?
14
+ raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}" unless result.success?
15
+
16
+ result.to_h
15
17
  end
16
18
 
17
19
  # Validate instance attributes using the defined validation contract
@@ -23,7 +25,7 @@ module DhanHQ
23
25
 
24
26
  result = contract.call(@attributes)
25
27
  @errors = result.errors.to_h unless result.success?
26
- raise DhanHQ::Error, "Validation Error: #{@errors}" unless valid?
28
+ raise DhanHQ::ValidationError, "Invalid parameters: #{@errors}" unless valid?
27
29
  end
28
30
 
29
31
  # Checks if the current instance is valid
@@ -8,7 +8,7 @@ module DhanHQ
8
8
  class AlertOrder < BaseModel
9
9
  include Concerns::ApiResponseHandler
10
10
 
11
- HTTP_PATH = "/alerts/orders"
11
+ HTTP_PATH = "/v2/alerts/orders"
12
12
 
13
13
  attributes :alert_id, :exchange_segment, :security_id, :condition,
14
14
  :trigger_price, :order_type, :transaction_type, :quantity,
@@ -37,6 +37,8 @@ module DhanHQ
37
37
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
38
38
 
39
39
  payload = response.is_a?(Array) ? response.first : response
40
+ return nil if payload.is_a?(Hash) && payload.empty?
41
+
40
42
  new(payload, skip_validation: true)
41
43
  end
42
44
 
@@ -65,7 +67,9 @@ module DhanHQ
65
67
  #
66
68
  def modify(alert_id, params)
67
69
  normalized = snake_case(params)
68
- response = resource.update(alert_id, camelize_keys(normalized))
70
+ validate_params!(normalized, DhanHQ::Contracts::AlertOrderContract)
71
+ payload = normalized.merge(alert_id: alert_id)
72
+ response = resource.update(alert_id, camelize_keys(payload))
69
73
  return nil unless success_response?(response)
70
74
 
71
75
  find(alert_id)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../contracts/edis_contract"
4
+
3
5
  module DhanHQ
4
6
  module Models
5
7
  ##
@@ -16,15 +18,15 @@ module DhanHQ
16
18
  # isin: "INE155A01022",
17
19
  # qty: 10,
18
20
  # exchange: "NSE",
19
- # segment: "E",
21
+ # segment: "EQ",
20
22
  # bulk: false
21
23
  # )
22
24
  #
23
- # @example Check EDIS status for a security
25
+ # @example Check EDIS status for a security (or "ALL" for all holdings)
24
26
  # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
25
27
  #
26
28
  class Edis < BaseModel
27
- HTTP_PATH = "/edis"
29
+ HTTP_PATH = "/v2/edis"
28
30
 
29
31
  class << self
30
32
  ##
@@ -52,24 +54,26 @@ module DhanHQ
52
54
  ##
53
55
  # Generate an eDIS form for authorizing sale of holdings.
54
56
  #
55
- # @param isin [String] ISIN of the security (e.g., "INE155A01022")
57
+ # @param isin [String] ISIN of the security (e.g. "INE733E01010")
56
58
  # @param qty [Integer] Quantity to authorize for sale
57
- # @param exchange [String] Exchange name (e.g., "NSE", "BSE")
58
- # @param segment [String] Segment identifier (e.g., "E")
59
- # @param bulk [Boolean] Whether this is a bulk authorization (default: false)
59
+ # @param exchange [String] Exchange: "NSE" or "BSE"
60
+ # @param segment [String] Segment: "EQ", "COMM", or "FNO"
61
+ # @param bulk [Boolean] If true, mark eDIS for all stocks in portfolio (default: false)
60
62
  #
61
- # @return [Hash] API response containing the eDIS form data
63
+ # @return [Hash] API response with dhanClientId and edisFormHtml (escaped HTML to render)
62
64
  #
63
65
  # @example Authorize sale of 10 shares
64
66
  # DhanHQ::Models::Edis.generate_form(
65
67
  # isin: "INE155A01022",
66
68
  # qty: 10,
67
69
  # exchange: "NSE",
68
- # segment: "E"
70
+ # segment: "EQ"
69
71
  # )
70
72
  #
71
73
  def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
72
- resource.form({ isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk })
74
+ params = { isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk }
75
+ validate_params!(params, DhanHQ::Contracts::EdisFormContract)
76
+ resource.form(params)
73
77
  end
74
78
 
75
79
  ##
@@ -85,13 +89,16 @@ module DhanHQ
85
89
  ##
86
90
  # Check EDIS authorization status for a security.
87
91
  #
88
- # @param isin [String] ISIN of the security to check
92
+ # @param isin [String] ISIN of the security, or "ALL" for all holdings
89
93
  #
90
- # @return [Hash] API response containing authorization status
94
+ # @return [Hash] API response with clientId, isin, totalQty, aprvdQty, status, remarks
91
95
  #
92
- # @example Check if EDIS is authorized
96
+ # @example Check if EDIS is authorized for one security
93
97
  # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
94
98
  #
99
+ # @example Check EDIS status for all holdings
100
+ # status = DhanHQ::Models::Edis.inquire(isin: "ALL")
101
+ #
95
102
  def inquire(isin:)
96
103
  resource.inquire(isin)
97
104
  end
@@ -42,7 +42,12 @@ module DhanHQ
42
42
  # call_data = data.call_data
43
43
  # put_data = data.put_data
44
44
  #
45
+ # @example Normalize to candles
46
+ # candles = data.to_candles
47
+ #
45
48
  class ExpiredOptionsData < BaseModel
49
+ OHLC_FIELDS = %i[open high low close iv volume strike spot oi open_interest].freeze
50
+
46
51
  # All expired options data attributes
47
52
  attributes :exchange_segment, :interval, :security_id, :instrument,
48
53
  :expiry_flag, :expiry_code, :strike, :drv_option_type,
@@ -56,63 +61,29 @@ module DhanHQ
56
61
  # 31 days in a single request. Historical data is available for up to the last 5 years.
57
62
  #
58
63
  # @param params [Hash{Symbol => String, Integer, Array<String>}] Request parameters
64
+ # @option params [String, Integer] :security_id (required) Underlying exchange standard ID for each scrip
59
65
  # @option params [String] :exchange_segment (required) Exchange and segment identifier.
60
- # Valid values: "NSE_FNO", "BSE_FNO", "NSE_EQ", "BSE_EQ"
61
- # @option params [String] :interval (required) Minute intervals for the timeframe.
62
- # Valid values: "1", "5", "15", "25", "60"
63
- # @option params [Integer] :security_id (required) Underlying exchange standard ID for each scrip
66
+ # Valid values: "NSE_FNO", "IDX_I", "NSE_EQ", "BSE_EQ"
64
67
  # @option params [String] :instrument (required) Instrument type of the scrip.
65
68
  # Valid values: "OPTIDX" (Index Options), "OPTSTK" (Stock Options)
69
+ # @option params [String, Integer] :interval (required) Minute intervals for the timeframe.
70
+ # Valid values: "1", "5", "15", "25", "60"
66
71
  # @option params [String] :expiry_flag (required) Expiry interval of the instrument.
67
72
  # Valid values: "WEEK", "MONTH"
68
73
  # @option params [Integer] :expiry_code (required) Expiry code for the instrument
69
74
  # @option params [String] :strike (required) Strike price specification.
70
75
  # Format: "ATM" for At The Money, "ATM+X" or "ATM-X" for offset strikes.
71
- # For Index Options (near expiry): Up to ATM+10 / ATM-10
72
- # For all other contracts: Up to ATM+3 / ATM-3
73
- # @option params [String] :drv_option_type (required) Option type.
74
- # Valid values: "CALL", "PUT"
76
+ # @option params [String] :option_type (required) Option type ("CALL" or "PUT").
75
77
  # @option params [Array<String>] :required_data (required) Array of required data fields.
76
- # Valid values: "open", "high", "low", "close", "iv", "volume", "strike", "oi", "spot"
77
- # @option params [String] :from_date (required) Start date of the desired range in YYYY-MM-DD format.
78
- # Cannot be more than 5 years ago. Same-day ranges are allowed.
79
- # @option params [String] :to_date (required) End date of the desired range (non-inclusive) in YYYY-MM-DD format.
80
- # Date range cannot exceed 31 days from from_date (to_date is non-inclusive). Same-day `from_date`/`to_date` is valid.
78
+ # @option params [String] :from_date (required) Start date in YYYY-MM-DD format.
79
+ # @option params [String] :to_date (required) End date in YYYY-MM-DD format.
81
80
  #
82
81
  # @return [ExpiredOptionsData] Expired options data object with fetched data
83
- #
84
- # @example Fetch NIFTY index options data
85
- # data = DhanHQ::Models::ExpiredOptionsData.fetch(
86
- # exchange_segment: "NSE_FNO",
87
- # interval: "1",
88
- # security_id: 13,
89
- # instrument: "OPTIDX",
90
- # expiry_flag: "MONTH",
91
- # expiry_code: 1,
92
- # strike: "ATM",
93
- # drv_option_type: "CALL",
94
- # required_data: ["open", "high", "low", "close", "volume", "iv", "oi", "spot"],
95
- # from_date: "2021-08-01",
96
- # to_date: "2021-09-01"
97
- # )
98
- #
99
- # @example Fetch stock options data for ATM+2 strike
100
- # data = DhanHQ::Models::ExpiredOptionsData.fetch(
101
- # exchange_segment: "NSE_FNO",
102
- # interval: "15",
103
- # security_id: 11536,
104
- # instrument: "OPTSTK",
105
- # expiry_flag: "WEEK",
106
- # expiry_code: 0,
107
- # strike: "ATM+2",
108
- # drv_option_type: "PUT",
109
- # required_data: ["open", "high", "low", "close", "volume"],
110
- # from_date: "2024-01-01",
111
- # to_date: "2024-01-31"
112
- # )
113
- #
114
82
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
115
83
  def fetch(params)
84
+ # Map option_type to drv_option_type if provided
85
+ params[:drv_option_type] ||= params[:option_type] if params.key?(:option_type)
86
+
116
87
  normalized = normalize_params(params)
117
88
  validate_params(normalized)
118
89
 
@@ -120,6 +91,8 @@ module DhanHQ
120
91
  new(response.merge(normalized), skip_validation: true)
121
92
  end
122
93
 
94
+ alias rolling fetch
95
+
123
96
  private
124
97
 
125
98
  def expired_options_resource
@@ -180,6 +153,43 @@ module DhanHQ
180
153
  end
181
154
  end
182
155
 
156
+ ##
157
+ # Normalizes the columnar response into an array of candle hashes.
158
+ #
159
+ # @param option_type [String, nil] Option type to retrieve ("CALL" or "PUT").
160
+ # If nil, uses the {#drv_option_type} from the request.
161
+ # @return [Array<Hash>] Normalized array of candles.
162
+ def to_candles(option_type = nil)
163
+ option_type ||= drv_option_type
164
+ opt_data = data_for_type(option_type)
165
+ return [] unless opt_data.is_a?(Hash)
166
+
167
+ # Standardize keys to symbols
168
+ opt_data = opt_data.transform_keys(&:to_sym)
169
+ ts_arr = opt_data[:timestamp]
170
+ return [] unless ts_arr.is_a?(Array)
171
+
172
+ type_sym = option_type.to_s.downcase.to_sym
173
+
174
+ ts_arr.each_with_index.map do |ts, i|
175
+ candle = {
176
+ option_type: type_sym,
177
+ timestamp: ts.is_a?(Numeric) ? Time.at(ts) : ts
178
+ }
179
+
180
+ # Map requested fields
181
+ OHLC_FIELDS.each do |field|
182
+ val_arr = opt_data[field]
183
+ next unless val_arr.is_a?(Array)
184
+
185
+ # Map 'oi' to 'open_interest' if requested
186
+ target_field = field == :oi ? :open_interest : field
187
+ candle[target_field] = val_arr[i]
188
+ end
189
+ candle
190
+ end
191
+ end
192
+
183
193
  ##
184
194
  # Gets call option data from the response.
185
195
  #
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../contracts/forever_order_contract"
4
+
3
5
  module DhanHQ
4
6
  module Models
5
7
  ##
@@ -154,9 +156,9 @@ module DhanHQ
154
156
  # @option params [String] :transaction_type (required) The trading side of transaction.
155
157
  # Valid values: "BUY", "SELL"
156
158
  # @option params [String] :exchange_segment (required) Exchange and segment identifier.
157
- # Valid values: "NSE_EQ", "NSE_FNO", "BSE_EQ", "BSE_FNO"
159
+ # Valid values: See {DhanHQ::Constants::FOREVER_ORDER_SEGMENTS} (NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM)
158
160
  # @option params [String] :product_type (required) Product type.
159
- # Valid values: "CNC", "MTF"
161
+ # Valid values: See {DhanHQ::Constants::FOREVER_ORDER_PRODUCT_TYPES} (CNC, MTF)
160
162
  # @option params [String] :order_type (required) Order type.
161
163
  # Valid values: "LIMIT", "MARKET"
162
164
  # @option params [String] :validity (required) Validity of order for execution.
@@ -216,10 +218,12 @@ module DhanHQ
216
218
  #
217
219
  # @note Order placement APIs require Static IP whitelisting
218
220
  def create(params)
219
- # Normalize params and auto-inject dhan_client_id from configuration if not provided
220
- normalized_params = snake_case(params)
221
- normalized_params[:dhan_client_id] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.client_id
222
- response = resource.create(normalized_params)
221
+ normalized = snake_case(params)
222
+ config = DhanHQ.configuration
223
+ normalized[:dhan_client_id] ||= config.client_id if config&.client_id
224
+ validate_params!(normalized, DhanHQ::Contracts::ForeverOrderCreateContract)
225
+ formatted = camelize_keys(normalized)
226
+ response = resource.create(formatted)
223
227
  return nil unless response.is_a?(Hash) && response["orderId"]
224
228
 
225
229
  find(response["orderId"])
@@ -291,7 +295,13 @@ module DhanHQ
291
295
  raise "Order ID is required to modify a forever order" unless order_id
292
296
 
293
297
  DhanHQ.logger&.info("[DhanHQ::Models::ForeverOrder] Modifying order #{order_id}")
294
- response = self.class.resource.update(order_id, new_params)
298
+ full_params = snake_case(new_params)
299
+ config = DhanHQ.configuration
300
+ full_params[:dhan_client_id] ||= config.client_id if config&.client_id
301
+ full_params[:order_id] = order_id
302
+ validate_params!(full_params, DhanHQ::Contracts::ForeverOrderModifyContract)
303
+ formatted = camelize_keys(full_params)
304
+ response = self.class.resource.update(order_id, formatted)
295
305
  ctx = "[DhanHQ::Models::ForeverOrder] Modification"
296
306
  success = handle_api_response(response, success_key: "orderId", context: ctx)
297
307
  return self.class.find(order_id) if success