DhanHQ 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/GUIDE.md +555 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +463 -0
  9. data/README1.md +521 -0
  10. data/Rakefile +12 -0
  11. data/TAGS +10 -0
  12. data/TODO-1.md +14 -0
  13. data/TODO.md +127 -0
  14. data/app/services/live/order_update_guard_support.rb +75 -0
  15. data/app/services/live/order_update_hub.rb +76 -0
  16. data/app/services/live/order_update_persistence_support.rb +68 -0
  17. data/config/initializers/order_update_hub.rb +16 -0
  18. data/diagram.html +184 -0
  19. data/diagram.md +34 -0
  20. data/docs/rails_integration.md +304 -0
  21. data/exe/DhanHQ +4 -0
  22. data/lib/DhanHQ/client.rb +116 -0
  23. data/lib/DhanHQ/config.rb +32 -0
  24. data/lib/DhanHQ/configuration.rb +72 -0
  25. data/lib/DhanHQ/constants.rb +170 -0
  26. data/lib/DhanHQ/contracts/base_contract.rb +15 -0
  27. data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
  28. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
  29. data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
  30. data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
  31. data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
  32. data/lib/DhanHQ/contracts/order_contract.rb +102 -0
  33. data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
  34. data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
  35. data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
  36. data/lib/DhanHQ/core/base_api.rb +105 -0
  37. data/lib/DhanHQ/core/base_model.rb +266 -0
  38. data/lib/DhanHQ/core/base_resource.rb +50 -0
  39. data/lib/DhanHQ/core/error_handler.rb +19 -0
  40. data/lib/DhanHQ/error_object.rb +49 -0
  41. data/lib/DhanHQ/errors.rb +45 -0
  42. data/lib/DhanHQ/helpers/api_helper.rb +17 -0
  43. data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
  44. data/lib/DhanHQ/helpers/model_helper.rb +7 -0
  45. data/lib/DhanHQ/helpers/request_helper.rb +69 -0
  46. data/lib/DhanHQ/helpers/response_helper.rb +98 -0
  47. data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
  48. data/lib/DhanHQ/json_loader.rb +23 -0
  49. data/lib/DhanHQ/models/edis.rb +58 -0
  50. data/lib/DhanHQ/models/forever_order.rb +85 -0
  51. data/lib/DhanHQ/models/funds.rb +50 -0
  52. data/lib/DhanHQ/models/historical_data.rb +77 -0
  53. data/lib/DhanHQ/models/holding.rb +56 -0
  54. data/lib/DhanHQ/models/kill_switch.rb +49 -0
  55. data/lib/DhanHQ/models/ledger_entry.rb +60 -0
  56. data/lib/DhanHQ/models/margin.rb +54 -0
  57. data/lib/DhanHQ/models/market_feed.rb +41 -0
  58. data/lib/DhanHQ/models/option_chain.rb +79 -0
  59. data/lib/DhanHQ/models/order.rb +239 -0
  60. data/lib/DhanHQ/models/position.rb +60 -0
  61. data/lib/DhanHQ/models/profile.rb +44 -0
  62. data/lib/DhanHQ/models/super_order.rb +69 -0
  63. data/lib/DhanHQ/models/trade.rb +79 -0
  64. data/lib/DhanHQ/rate_limiter.rb +107 -0
  65. data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
  66. data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
  67. data/lib/DhanHQ/requests/orders/create.json +0 -0
  68. data/lib/DhanHQ/resources/edis.rb +44 -0
  69. data/lib/DhanHQ/resources/forever_orders.rb +53 -0
  70. data/lib/DhanHQ/resources/funds.rb +21 -0
  71. data/lib/DhanHQ/resources/historical_data.rb +34 -0
  72. data/lib/DhanHQ/resources/holdings.rb +21 -0
  73. data/lib/DhanHQ/resources/kill_switch.rb +21 -0
  74. data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
  75. data/lib/DhanHQ/resources/market_feed.rb +56 -0
  76. data/lib/DhanHQ/resources/option_chain.rb +31 -0
  77. data/lib/DhanHQ/resources/orders.rb +70 -0
  78. data/lib/DhanHQ/resources/positions.rb +29 -0
  79. data/lib/DhanHQ/resources/profile.rb +25 -0
  80. data/lib/DhanHQ/resources/statements.rb +42 -0
  81. data/lib/DhanHQ/resources/super_orders.rb +46 -0
  82. data/lib/DhanHQ/resources/trades.rb +23 -0
  83. data/lib/DhanHQ/version.rb +6 -0
  84. data/lib/DhanHQ/ws/client.rb +182 -0
  85. data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
  86. data/lib/DhanHQ/ws/connection.rb +240 -0
  87. data/lib/DhanHQ/ws/decoder.rb +83 -0
  88. data/lib/DhanHQ/ws/errors.rb +0 -0
  89. data/lib/DhanHQ/ws/orders/client.rb +59 -0
  90. data/lib/DhanHQ/ws/orders/connection.rb +148 -0
  91. data/lib/DhanHQ/ws/orders.rb +13 -0
  92. data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
  93. data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
  94. data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
  95. data/lib/DhanHQ/ws/packets/header.rb +23 -0
  96. data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
  97. data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
  98. data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
  99. data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
  100. data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
  101. data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
  102. data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
  103. data/lib/DhanHQ/ws/registry.rb +46 -0
  104. data/lib/DhanHQ/ws/segments.rb +75 -0
  105. data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
  106. data/lib/DhanHQ/ws/sub_state.rb +59 -0
  107. data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
  108. data/lib/DhanHQ/ws.rb +37 -0
  109. data/lib/DhanHQ.rb +135 -0
  110. data/lib/ta/technical_analysis.rb +405 -0
  111. data/sig/DhanHQ.rbs +4 -0
  112. data/watchlist.csv +3 -0
  113. metadata +283 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Helper methods for normalising attribute keys across API responses.
5
+ module AttributeHelper
6
+ # Convert keys from snake_case to camelCase
7
+ #
8
+ # @param hash [Hash] The hash to convert
9
+ # @return [Hash] The camelCased hash
10
+ def camelize_keys(hash)
11
+ hash.transform_keys { |key| key.to_s.camelize(:lower) }
12
+ end
13
+
14
+ # Convert keys from snake_case to TitleCase
15
+ #
16
+ # @param hash [Hash] The hash to convert
17
+ # @return [Hash] The TitleCased hash
18
+ def titleize_keys(hash)
19
+ hash.transform_keys { |key| key.to_s.titleize.delete(" ") }
20
+ end
21
+
22
+ # Convert keys from camelCase to snake_case
23
+ #
24
+ # @param hash [Hash] The hash to convert
25
+ # @return [Hash] The snake_cased hash
26
+ def snake_case(hash)
27
+ hash.transform_keys { |key| key.to_s.underscore.to_sym }
28
+ end
29
+
30
+ # Normalize attribute keys to be accessible as both snake_case and camelCase
31
+ #
32
+ # @param hash [Hash] The attributes hash
33
+ # @return [HashWithIndifferentAccess] The normalized attributes
34
+ def normalize_keys(hash)
35
+ hash.each_with_object({}) do |(key, value), result|
36
+ string_key = key.to_s
37
+ result[string_key] = value
38
+ result[string_key.underscore] = value
39
+ end.with_indifferent_access
40
+ end
41
+
42
+ # Override `inspect` to display instance variables instead of attributes hash
43
+ #
44
+ # @return [String] Readable debug output for the object
45
+ def inspect
46
+ instance_vars = self.class.defined_attributes.map { |attr| "#{attr}: #{instance_variable_get(:"@#{attr}")}" }
47
+ "#<#{self.class.name} #{instance_vars.join(", ")}>"
48
+ end
49
+
50
+ # def format_params(path, params)
51
+ # return params unless params.is_a?(Hash)
52
+
53
+ # if optionchain_api?(path)
54
+ # titleize_keys(params)
55
+ # else
56
+ # camelize_keys(params)
57
+ # end
58
+ # end
59
+
60
+ # def camelize_keys(hash)
61
+ # hash.transform_keys { |key| key.to_s.camelize(:lower) }
62
+ # end
63
+
64
+ # def titleize_keys(hash)
65
+ # hash.transform_keys { |key| key.to_s.titleize.delete(" ") }
66
+ # end
67
+
68
+ # def optionchain_api?(path)
69
+ # path.include?("/optionchain")
70
+ # end
71
+ end
72
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Placeholder namespace for future model-level helpers.
5
+ module ModelHelper
6
+ end
7
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Helper mixin used by models and clients to assemble API requests.
5
+ module RequestHelper
6
+ # Builds a model object from API response
7
+ #
8
+ # @param response [Hash] API response
9
+ # @return [DhanHQ::BaseModel, DhanHQ::ErrorObject]
10
+ def build_from_response(response)
11
+ return DhanHQ::ErrorObject.new(response) unless success_response?(response)
12
+
13
+ attributes = if response.is_a?(Hash) && response[:data].is_a?(Hash)
14
+ response[:data]
15
+ else
16
+ response
17
+ end
18
+
19
+ new(attributes, skip_validation: true)
20
+ end
21
+
22
+ private
23
+
24
+ # Dynamically builds headers for each request.
25
+ #
26
+ # @param path [String] The API endpoint path.
27
+ # @return [Hash] The request headers.
28
+ def build_headers(path)
29
+ headers = {
30
+ "Content-Type" => "application/json",
31
+ "Accept" => "application/json",
32
+ "access-token" => DhanHQ.configuration.access_token
33
+ }
34
+
35
+ # Add client-id for DATA APIs
36
+ headers["client-id"] = DhanHQ.configuration.client_id if data_api?(path)
37
+
38
+ headers
39
+ end
40
+
41
+ # Determines if the API path requires a `client-id` header.
42
+ #
43
+ # @param path [String] The API endpoint path.
44
+ # @return [Boolean] True if the path belongs to a DATA API.
45
+ def data_api?(path)
46
+ DhanHQ::Constants::DATA_API_PATHS.include?(path)
47
+ end
48
+
49
+ # Prepares the request payload based on the HTTP method.
50
+ #
51
+ # @param req [Faraday::Request] The request object.
52
+ # @param payload [Hash] The request payload.
53
+ # @param method [Symbol] The HTTP method.
54
+ def prepare_payload(req, payload, method)
55
+ return if payload.nil? || payload.empty?
56
+
57
+ unless payload.is_a?(Hash)
58
+ raise DhanHQ::InputExceptionError,
59
+ "Invalid payload: Expected a Hash, got #{payload.class}"
60
+ end
61
+
62
+ case method
63
+ when :delete then req.params = {}
64
+ when :get then req.params = payload
65
+ else req.body = payload.to_json
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Helper mixin for normalising API responses and raising mapped errors.
5
+ module ResponseHelper
6
+ private
7
+
8
+ # Determines if the API response indicates success.
9
+ #
10
+ # Treat responses missing a `:status` key but containing
11
+ # an `orderId` or `orderStatus` as successful. This aligns with
12
+ # certain Dhan APIs which return only order details on success.
13
+ #
14
+ # @param response [Hash] Parsed API response
15
+ # @return [Boolean] True when the response signifies success
16
+ def success_response?(response)
17
+ return false unless response.is_a?(Hash)
18
+
19
+ return true if response[:status] == "success"
20
+ return true if response[:status].nil? && (response.key?(:orderId) || response.key?(:orderStatus))
21
+
22
+ false
23
+ end
24
+
25
+ # Handles the API response.
26
+ #
27
+ # @param response [Faraday::Response] The raw response object.
28
+ # @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>] The parsed response.
29
+ # @raise [DhanHQ::Error] If an HTTP error occurs.
30
+ def handle_response(response)
31
+ case response.status
32
+ when 200..299 then parse_json(response.body)
33
+ else handle_error(response)
34
+ end
35
+ end
36
+
37
+ # Handles standard HTTP errors.
38
+ #
39
+ # @param response [Faraday::Response] The raw response object.
40
+ # @raise [DhanHQ::Error] The specific error based on response status.
41
+ def handle_error(response)
42
+ body = parse_json(response.body)
43
+
44
+ error_code = body[:errorCode] || response.status.to_s
45
+ error_message = body[:errorMessage] || body[:message] || "Unknown error"
46
+ if error_code == "DH-1111"
47
+ error_message = "No holdings found for this account. Add holdings or wait for them to settle before retrying."
48
+ end
49
+
50
+ error_class = DhanHQ::Constants::DHAN_ERROR_MAPPING[error_code]
51
+
52
+ error_class ||=
53
+ case response.status
54
+ when 400 then DhanHQ::InputExceptionError
55
+ when 401 then DhanHQ::InvalidAuthenticationError
56
+ when 403 then DhanHQ::InvalidAccessError
57
+ when 404 then DhanHQ::NotFoundError
58
+ when 429 then DhanHQ::RateLimitError
59
+ when 500..599 then DhanHQ::InternalServerError
60
+ else DhanHQ::OtherError
61
+ end
62
+
63
+ error_text =
64
+ if error_code == "DH-1111"
65
+ "#{error_message} (error code: #{error_code})"
66
+ else
67
+ "#{error_code}: #{error_message}"
68
+ end
69
+
70
+ raise error_class, error_text
71
+ end
72
+
73
+ # Parses JSON response safely. Converts response body to a hash or array with indifferent access.
74
+ #
75
+ # @param body [String, Hash] The response body.
76
+ # @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>] The parsed JSON.
77
+ def parse_json(body)
78
+ parsed_body =
79
+ if body.is_a?(String)
80
+ begin
81
+ JSON.parse(body, symbolize_names: true)
82
+ rescue JSON::ParserError
83
+ {} # Return an empty hash if the string is not valid JSON
84
+ end
85
+ else
86
+ body
87
+ end
88
+
89
+ if parsed_body.is_a?(Hash)
90
+ parsed_body.with_indifferent_access
91
+ elsif parsed_body.is_a?(Array)
92
+ parsed_body.map(&:with_indifferent_access)
93
+ else
94
+ parsed_body
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Helper methods for running Dry::Validation contracts against payloads.
5
+ module ValidationHelper
6
+ # Validate the attributes using the validation contract
7
+ #
8
+ # @param params [Hash] The parameters to validate
9
+ # @param contract_class [Class] The contract class to use for validation
10
+ def validate_params!(params, contract_class)
11
+ contract = contract_class.new
12
+ result = contract.call(params)
13
+
14
+ raise DhanHQ::Error, "Validation Error: #{result.errors.to_h}" unless result.success?
15
+ end
16
+
17
+ # Validate instance attributes using the defined validation contract
18
+ def validate!
19
+ contract_class = respond_to?(:validation_contract) ? validation_contract : self.class.validation_contract
20
+ return unless contract_class
21
+
22
+ contract = contract_class.is_a?(Class) ? contract_class.new : contract_class
23
+
24
+ result = contract.call(@attributes)
25
+ @errors = result.errors.to_h unless result.success?
26
+ raise DhanHQ::Error, "Validation Error: #{@errors}" unless valid?
27
+ end
28
+
29
+ # Checks if the current instance is valid
30
+ #
31
+ # @return [Boolean] True if the model is valid
32
+ def valid?
33
+ @errors.empty?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module DhanHQ
6
+ # Utility for loading canned JSON fixtures bundled with the gem.
7
+ module JSONLoader
8
+ # Loads and symbolises a JSON request payload from the `requests/` folder.
9
+ #
10
+ # @param file [String] Relative path to the fixture file.
11
+ # @return [Hash] Parsed JSON payload with symbolised keys.
12
+ def self.load(file)
13
+ file_path = File.expand_path("requests/#{file}", __dir__)
14
+ JSON.parse(File.read(file_path), symbolize_names: true)
15
+ rescue Errno::ENOENT
16
+ puts "File not found: #{file_path}"
17
+ {}
18
+ rescue JSON::ParserError
19
+ puts "Invalid JSON format in #{file_path}"
20
+ {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model wrapper for electronic DIS flows.
6
+ class Edis < BaseModel
7
+ # Base path backing the model operations.
8
+ HTTP_PATH = "/v2/edis"
9
+
10
+ class << self
11
+ # Shared resource client used by the model helpers.
12
+ #
13
+ # @return [DhanHQ::Resources::Edis]
14
+ def resource
15
+ @resource ||= DhanHQ::Resources::Edis.new
16
+ end
17
+
18
+ # Submits an EDIS form request.
19
+ #
20
+ # @param params [Hash]
21
+ # @return [Hash]
22
+ def form(params)
23
+ resource.form(params)
24
+ end
25
+
26
+ # Submits a bulk EDIS form request.
27
+ #
28
+ # @param params [Hash]
29
+ # @return [Hash]
30
+ def bulk_form(params)
31
+ resource.bulk_form(params)
32
+ end
33
+
34
+ # Requests a TPIN for the configured client.
35
+ #
36
+ # @return [Hash]
37
+ def tpin
38
+ resource.tpin
39
+ end
40
+
41
+ # Inquires EDIS status for a specific ISIN.
42
+ #
43
+ # @param isin [String]
44
+ # @return [Hash]
45
+ def inquire(isin)
46
+ resource.inquire(isin)
47
+ end
48
+ end
49
+
50
+ # EDIS payloads are validated upstream so no contract is applied.
51
+ #
52
+ # @return [nil]
53
+ def validation_contract
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # ActiveModel-style wrapper for Good Till Trigger/Forever orders.
6
+ class ForeverOrder < BaseModel
7
+ attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
8
+ :transaction_type, :exchange_segment, :product_type, :order_flag,
9
+ :order_type, :validity, :trading_symbol, :security_id, :quantity,
10
+ :disclosed_quantity, :price, :trigger_price, :price1,
11
+ :trigger_price1, :quantity1, :leg_name, :create_time,
12
+ :update_time, :exchange_time, :drv_expiry_date, :drv_option_type,
13
+ :drv_strike_price
14
+
15
+ class << self
16
+ # Provides a shared instance of the ForeverOrders resource
17
+ #
18
+ # @return [DhanHQ::Resources::ForeverOrders]
19
+ def resource
20
+ @resource ||= DhanHQ::Resources::ForeverOrders.new
21
+ end
22
+
23
+ ##
24
+ # Fetch all forever orders
25
+ #
26
+ # @return [Array<ForeverOrder>]
27
+ def all
28
+ response = resource.all
29
+ return [] unless response.is_a?(Array)
30
+
31
+ response.map { |o| new(o, skip_validation: true) }
32
+ end
33
+
34
+ ##
35
+ # Retrieve a specific forever order
36
+ #
37
+ # @param order_id [String]
38
+ # @return [ForeverOrder, nil]
39
+ def find(order_id)
40
+ response = resource.find(order_id)
41
+ return nil unless response.is_a?(Hash) && response.any?
42
+
43
+ new(response, skip_validation: true)
44
+ end
45
+
46
+ ##
47
+ # Create a new forever order
48
+ #
49
+ # @param params [Hash]
50
+ # @return [ForeverOrder, nil]
51
+ def create(params)
52
+ response = resource.create(params)
53
+ return nil unless response.is_a?(Hash) && response["orderId"]
54
+
55
+ find(response["orderId"])
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Modify an existing forever order
61
+ #
62
+ # @param new_params [Hash]
63
+ # @return [ForeverOrder, nil]
64
+ def modify(new_params)
65
+ raise "Order ID is required to modify a forever order" unless id
66
+
67
+ response = self.class.resource.update(id, new_params)
68
+ return self.class.find(id) if self.class.send(:success_response?, response)
69
+
70
+ nil
71
+ end
72
+
73
+ ##
74
+ # Cancel the forever order
75
+ #
76
+ # @return [Boolean]
77
+ def cancel
78
+ raise "Order ID is required to cancel a forever order" unless id
79
+
80
+ response = self.class.resource.cancel(id)
81
+ response["orderStatus"] == "CANCELLED"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model representing the funds/limits endpoint response.
6
+ class Funds < BaseModel
7
+ # Base path used by the funds resource.
8
+ HTTP_PATH = "/v2/fundlimit"
9
+
10
+ attributes :available_balance, :sod_limit, :collateral_amount, :receiveable_amount, :utilized_amount,
11
+ :blocked_payout_amount, :withdrawable_balance
12
+
13
+ # The API currently returns the key `availabelBalance` (note the typo).
14
+ # To maintain backwards compatibility while exposing a correctly
15
+ # spelled attribute, map the API response to `available_balance`.
16
+ def assign_attributes
17
+ if @attributes.key?(:availabel_balance) && !@attributes.key?(:available_balance)
18
+ @attributes[:available_balance] = @attributes[:availabel_balance]
19
+ end
20
+ super
21
+ end
22
+ class << self
23
+ ##
24
+ # Provides a **shared instance** of the `Funds` resource.
25
+ #
26
+ # @return [DhanHQ::Resources::Funds]
27
+ def resource
28
+ @resource ||= DhanHQ::Resources::Funds.new
29
+ end
30
+
31
+ ##
32
+ # Fetch fund details.
33
+ #
34
+ # @return [Fund]
35
+ def fetch
36
+ response = resource.fetch
37
+ new(response, skip_validation: true)
38
+ end
39
+
40
+ ##
41
+ # Fetch only the available balance.
42
+ #
43
+ # @return [Float] Available balance in the trading account.
44
+ def balance
45
+ fetch.available_balance
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Model class for fetching Daily & Intraday data
7
+ # The default response is a Hash with arrays of "open", "high", "low", etc.
8
+ #
9
+ class HistoricalData < BaseModel
10
+ # Typically, we won't define a single resource path,
11
+ # because we call "daily" or "intraday" endpoints specifically.
12
+ # So let's rely on the resource call directly.
13
+ HTTP_PATH = "/v2/charts"
14
+
15
+ # If you want typed attributes, you could define them,
16
+ # but the endpoints return arrays. We'll keep it raw.
17
+ # e.g. attributes :open, :high, :low, :close, :volume, :timestamp
18
+
19
+ class << self
20
+ ##
21
+ # Provide a **shared instance** of the `HistoricalData` resource
22
+ #
23
+ # @return [DhanHQ::Resources::HistoricalData]
24
+ def resource
25
+ @resource ||= DhanHQ::Resources::HistoricalData.new
26
+ end
27
+
28
+ ##
29
+ # Daily historical data
30
+ # @param params [Hash] The request parameters, e.g.:
31
+ # {
32
+ # security_id: "1333",
33
+ # exchange_segment: "NSE_EQ",
34
+ # instrument: "EQUITY",
35
+ # expiry_code: 0,
36
+ # from_date: "2022-01-08",
37
+ # to_date: "2022-02-08"
38
+ # }
39
+ # @return [HashWithIndifferentAccess]
40
+ # {
41
+ # open: [...], high: [...], low: [...], close: [...],
42
+ # volume: [...], timestamp: [...]
43
+ # }
44
+ def daily(params)
45
+ validate_params!(params, DhanHQ::Contracts::HistoricalDataContract)
46
+ # You can rename the keys from snake_case to something if needed
47
+ resource.daily(params)
48
+ # return as a raw hash or transform further
49
+ end
50
+
51
+ ##
52
+ # Intraday historical data
53
+ # @param params [Hash], e.g.:
54
+ # {
55
+ # security_id: "1333",
56
+ # exchange_segment: "NSE_EQ",
57
+ # instrument: "EQUITY",
58
+ # interval: "15",
59
+ # from_date: "2024-09-11",
60
+ # to_date: "2024-09-15"
61
+ # }
62
+ # @return [HashWithIndifferentAccess]
63
+ # { open: [...], high: [...], low: [...], close: [...],
64
+ # volume: [...], timestamp: [...] }
65
+ def intraday(params)
66
+ validate_params!(params, DhanHQ::Contracts::HistoricalDataContract)
67
+ resource.intraday(params)
68
+ end
69
+ end
70
+
71
+ # For a read-only type of data, we might skip validations or specify a contract if needed
72
+ def validation_contract
73
+ nil
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model representing a single portfolio holding.
6
+ class Holding < BaseModel
7
+ # Base path used when retrieving holdings.
8
+ HTTP_PATH = "/v2/holdings"
9
+
10
+ attributes :exchange, :trading_symbol, :security_id, :isin, :total_qty,
11
+ :dp_qty, :t1_qty, :available_qty, :collateral_qty, :avg_cost_price
12
+
13
+ class << self
14
+ ##
15
+ # Provides a **shared instance** of the `Holdings` resource.
16
+ #
17
+ # @return [DhanHQ::Resources::Holdings]
18
+ def resource
19
+ @resource ||= DhanHQ::Resources::Holdings.new
20
+ end
21
+
22
+ ##
23
+ # Fetch all holdings.
24
+ #
25
+ # @return [Array<Holding>]
26
+ def all
27
+ response = resource.all
28
+ return [] unless response.is_a?(Array)
29
+
30
+ response.map { |holding| new(holding, skip_validation: true) }
31
+ rescue DhanHQ::NoHoldingsError
32
+ []
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Convert model attributes to a hash.
38
+ #
39
+ # @return [Hash] Hash representation of the Holding model.
40
+ def to_h
41
+ {
42
+ exchange: exchange,
43
+ trading_symbol: trading_symbol,
44
+ security_id: security_id,
45
+ isin: isin,
46
+ total_qty: total_qty,
47
+ dp_qty: dp_qty,
48
+ t1_qty: t1_qty,
49
+ available_qty: available_qty,
50
+ collateral_qty: collateral_qty,
51
+ avg_cost_price: avg_cost_price
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model helper to toggle the trading kill switch.
6
+ class KillSwitch < BaseModel
7
+ # Base path used by the kill switch resource.
8
+ HTTP_PATH = "/v2/killswitch"
9
+
10
+ class << self
11
+ # Shared resource for kill switch operations.
12
+ #
13
+ # @return [DhanHQ::Resources::KillSwitch]
14
+ def resource
15
+ @resource ||= DhanHQ::Resources::KillSwitch.new
16
+ end
17
+
18
+ # Updates the kill switch status.
19
+ #
20
+ # @param status [String]
21
+ # @return [Hash]
22
+ def update(status)
23
+ resource.update(kill_switch_status: status)
24
+ end
25
+
26
+ # Activates the kill switch for the account.
27
+ #
28
+ # @return [Hash]
29
+ def activate
30
+ update("ACTIVATE")
31
+ end
32
+
33
+ # Deactivates the kill switch for the account.
34
+ #
35
+ # @return [Hash]
36
+ def deactivate
37
+ update("DEACTIVATE")
38
+ end
39
+ end
40
+
41
+ # No explicit validation contract is required for kill switch updates.
42
+ #
43
+ # @return [nil]
44
+ def validation_contract
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end