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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +20 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +555 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/README1.md +521 -0
- data/Rakefile +12 -0
- data/TAGS +10 -0
- data/TODO-1.md +14 -0
- data/TODO.md +127 -0
- data/app/services/live/order_update_guard_support.rb +75 -0
- data/app/services/live/order_update_hub.rb +76 -0
- data/app/services/live/order_update_persistence_support.rb +68 -0
- data/config/initializers/order_update_hub.rb +16 -0
- data/diagram.html +184 -0
- data/diagram.md +34 -0
- data/docs/rails_integration.md +304 -0
- data/exe/DhanHQ +4 -0
- data/lib/DhanHQ/client.rb +116 -0
- data/lib/DhanHQ/config.rb +32 -0
- data/lib/DhanHQ/configuration.rb +72 -0
- data/lib/DhanHQ/constants.rb +170 -0
- data/lib/DhanHQ/contracts/base_contract.rb +15 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
- data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
- data/lib/DhanHQ/contracts/order_contract.rb +102 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
- data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
- data/lib/DhanHQ/core/base_api.rb +105 -0
- data/lib/DhanHQ/core/base_model.rb +266 -0
- data/lib/DhanHQ/core/base_resource.rb +50 -0
- data/lib/DhanHQ/core/error_handler.rb +19 -0
- data/lib/DhanHQ/error_object.rb +49 -0
- data/lib/DhanHQ/errors.rb +45 -0
- data/lib/DhanHQ/helpers/api_helper.rb +17 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
- data/lib/DhanHQ/helpers/model_helper.rb +7 -0
- data/lib/DhanHQ/helpers/request_helper.rb +69 -0
- data/lib/DhanHQ/helpers/response_helper.rb +98 -0
- data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
- data/lib/DhanHQ/json_loader.rb +23 -0
- data/lib/DhanHQ/models/edis.rb +58 -0
- data/lib/DhanHQ/models/forever_order.rb +85 -0
- data/lib/DhanHQ/models/funds.rb +50 -0
- data/lib/DhanHQ/models/historical_data.rb +77 -0
- data/lib/DhanHQ/models/holding.rb +56 -0
- data/lib/DhanHQ/models/kill_switch.rb +49 -0
- data/lib/DhanHQ/models/ledger_entry.rb +60 -0
- data/lib/DhanHQ/models/margin.rb +54 -0
- data/lib/DhanHQ/models/market_feed.rb +41 -0
- data/lib/DhanHQ/models/option_chain.rb +79 -0
- data/lib/DhanHQ/models/order.rb +239 -0
- data/lib/DhanHQ/models/position.rb +60 -0
- data/lib/DhanHQ/models/profile.rb +44 -0
- data/lib/DhanHQ/models/super_order.rb +69 -0
- data/lib/DhanHQ/models/trade.rb +79 -0
- data/lib/DhanHQ/rate_limiter.rb +107 -0
- data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
- data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
- data/lib/DhanHQ/requests/orders/create.json +0 -0
- data/lib/DhanHQ/resources/edis.rb +44 -0
- data/lib/DhanHQ/resources/forever_orders.rb +53 -0
- data/lib/DhanHQ/resources/funds.rb +21 -0
- data/lib/DhanHQ/resources/historical_data.rb +34 -0
- data/lib/DhanHQ/resources/holdings.rb +21 -0
- data/lib/DhanHQ/resources/kill_switch.rb +21 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
- data/lib/DhanHQ/resources/market_feed.rb +56 -0
- data/lib/DhanHQ/resources/option_chain.rb +31 -0
- data/lib/DhanHQ/resources/orders.rb +70 -0
- data/lib/DhanHQ/resources/positions.rb +29 -0
- data/lib/DhanHQ/resources/profile.rb +25 -0
- data/lib/DhanHQ/resources/statements.rb +42 -0
- data/lib/DhanHQ/resources/super_orders.rb +46 -0
- data/lib/DhanHQ/resources/trades.rb +23 -0
- data/lib/DhanHQ/version.rb +6 -0
- data/lib/DhanHQ/ws/client.rb +182 -0
- data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
- data/lib/DhanHQ/ws/connection.rb +240 -0
- data/lib/DhanHQ/ws/decoder.rb +83 -0
- data/lib/DhanHQ/ws/errors.rb +0 -0
- data/lib/DhanHQ/ws/orders/client.rb +59 -0
- data/lib/DhanHQ/ws/orders/connection.rb +148 -0
- data/lib/DhanHQ/ws/orders.rb +13 -0
- data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
- data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
- data/lib/DhanHQ/ws/packets/header.rb +23 -0
- data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
- data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
- data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
- data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
- data/lib/DhanHQ/ws/registry.rb +46 -0
- data/lib/DhanHQ/ws/segments.rb +75 -0
- data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
- data/lib/DhanHQ/ws/sub_state.rb +59 -0
- data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
- data/lib/DhanHQ/ws.rb +37 -0
- data/lib/DhanHQ.rb +135 -0
- data/lib/ta/technical_analysis.rb +405 -0
- data/sig/DhanHQ.rbs +4 -0
- data/watchlist.csv +3 -0
- 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,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
|