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,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_contract"
|
4
|
+
|
5
|
+
module DhanHQ
|
6
|
+
module Contracts
|
7
|
+
# Validation contract for slicing an order into multiple parts for Dhanhq's API.
|
8
|
+
#
|
9
|
+
# This contract ensures all required parameters are provided and optional parameters
|
10
|
+
# meet the required constraints when they are specified. It validates:
|
11
|
+
# - Required fields for slicing orders.
|
12
|
+
# - Conditional logic for fields based on the provided values.
|
13
|
+
# - Constraints such as inclusion, numerical ranges, and string formats.
|
14
|
+
#
|
15
|
+
# Example usage:
|
16
|
+
# contract = Dhanhq::Contracts::SliceOrderContract.new
|
17
|
+
# result = contract.call(
|
18
|
+
# dhanClientId: "123456",
|
19
|
+
# transactionType: "BUY",
|
20
|
+
# exchangeSegment: "NSE_EQ",
|
21
|
+
# productType: "CNC",
|
22
|
+
# orderType: "LIMIT",
|
23
|
+
# validity: "DAY",
|
24
|
+
# securityId: "1001",
|
25
|
+
# quantity: 10
|
26
|
+
# )
|
27
|
+
# result.success? # => true or false
|
28
|
+
#
|
29
|
+
# @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
|
30
|
+
class SliceOrderContract < BaseContract
|
31
|
+
# Parameters and validation rules for the slicing order request.
|
32
|
+
#
|
33
|
+
# @!attribute [r] correlationId
|
34
|
+
# @return [String] Optional. Identifier for tracking, max length 25 characters.
|
35
|
+
# @!attribute [r] transactionType
|
36
|
+
# @return [String] Required. BUY or SELL.
|
37
|
+
# @!attribute [r] exchangeSegment
|
38
|
+
# @return [String] Required. The segment in which the order is placed.
|
39
|
+
# Must be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM.
|
40
|
+
# @!attribute [r] productType
|
41
|
+
# @return [String] Required. Product type for the order.
|
42
|
+
# Must be one of: CNC, INTRADAY, MARGIN, MTF, CO, BO.
|
43
|
+
# @!attribute [r] orderType
|
44
|
+
# @return [String] Required. Type of order.
|
45
|
+
# Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
|
46
|
+
# @!attribute [r] validity
|
47
|
+
# @return [String] Required. Validity of the order.
|
48
|
+
# Must be one of: DAY, IOC, GTC, GTD.
|
49
|
+
# @!attribute [r] securityId
|
50
|
+
# @return [String] Required. Security identifier for the order.
|
51
|
+
# @!attribute [r] quantity
|
52
|
+
# @return [Integer] Required. Quantity of the order, must be greater than 0.
|
53
|
+
# @!attribute [r] disclosedQuantity
|
54
|
+
# @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
|
55
|
+
# @!attribute [r] price
|
56
|
+
# @return [Float] Optional. Price for the order, must be > 0 if provided.
|
57
|
+
# @!attribute [r] triggerPrice
|
58
|
+
# @return [Float] Optional. Trigger price for stop-loss orders, must be > 0 if provided.
|
59
|
+
# @!attribute [r] afterMarketOrder
|
60
|
+
# @return [Boolean] Optional. Indicates if this is an after-market order.
|
61
|
+
# @!attribute [r] amoTime
|
62
|
+
# @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
|
63
|
+
# @!attribute [r] boProfitValue
|
64
|
+
# @return [Float] Optional. Profit value for Bracket Orders, must be > 0 if provided.
|
65
|
+
# @!attribute [r] boStopLossValue
|
66
|
+
# @return [Float] Optional. Stop-loss value for Bracket Orders, must be > 0 if provided.
|
67
|
+
# @!attribute [r] drvExpiryDate
|
68
|
+
# @return [String] Optional. Expiry date for derivative contracts.
|
69
|
+
# @!attribute [r] drvOptionType
|
70
|
+
# @return [String] Optional. Option type for derivatives, must be one of: CALL, PUT, NA.
|
71
|
+
# @!attribute [r] drvStrikePrice
|
72
|
+
# @return [Float] Optional. Strike price for options, must be > 0 if provided.
|
73
|
+
params do
|
74
|
+
optional(:correlationId).maybe(:string, max_size?: 25)
|
75
|
+
required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
|
76
|
+
required(:exchangeSegment).filled(:string,
|
77
|
+
included_in?: %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ BSE_FNO BSE_CURRENCY
|
78
|
+
MCX_COMM])
|
79
|
+
required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF CO BO])
|
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])
|
82
|
+
required(:securityId).filled(:string)
|
83
|
+
required(:quantity).filled(:integer, gt?: 0)
|
84
|
+
optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
|
85
|
+
optional(:price).maybe(:float, gt?: 0)
|
86
|
+
optional(:triggerPrice).maybe(:float, gt?: 0)
|
87
|
+
optional(:afterMarketOrder).maybe(:bool)
|
88
|
+
optional(:amoTime).maybe(:string, included_in?: %w[OPEN OPEN_30 OPEN_60])
|
89
|
+
optional(:boProfitValue).maybe(:float, gt?: 0)
|
90
|
+
optional(:boStopLossValue).maybe(:float, gt?: 0)
|
91
|
+
optional(:drvExpiryDate).maybe(:string)
|
92
|
+
optional(:drvOptionType).maybe(:string, included_in?: %w[CALL PUT NA])
|
93
|
+
optional(:drvStrikePrice).maybe(:float, gt?: 0)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Custom validation for trigger price when the order type is STOP_LOSS or STOP_LOSS_MARKET.
|
97
|
+
rule(:triggerPrice, :orderType) do
|
98
|
+
if values[:orderType].start_with?("STOP_LOSS") && !values[:triggerPrice]
|
99
|
+
key(:triggerPrice).failure("is required for orderType STOP_LOSS or STOP_LOSS_MARKET")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Custom validation for AMO time when the order is marked as after-market.
|
104
|
+
rule(:afterMarketOrder, :amoTime) do
|
105
|
+
if values[:afterMarketOrder] == true && !values[:amoTime]
|
106
|
+
key(:amoTime).failure("is required when afterMarketOrder is true")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Base class for all API resource classes.
|
5
|
+
# Delegates HTTP requests to {DhanHQ::Client} and exposes helpers shared by
|
6
|
+
# resource wrappers.
|
7
|
+
class BaseAPI
|
8
|
+
include DhanHQ::APIHelper
|
9
|
+
include DhanHQ::AttributeHelper
|
10
|
+
|
11
|
+
# Default API type used when a subclass does not override {#initialize}.
|
12
|
+
API_TYPE = :non_trading_api
|
13
|
+
# Root path prepended to each endpoint segment.
|
14
|
+
HTTP_PATH = ""
|
15
|
+
|
16
|
+
attr_reader :client
|
17
|
+
|
18
|
+
# Initializes the BaseAPI with the appropriate Client instance
|
19
|
+
#
|
20
|
+
# @param api_type [Symbol] API type (`:order_api`, `:data_api`, `:non_trading_api`)
|
21
|
+
def initialize(api_type: self.class::API_TYPE)
|
22
|
+
@client = DhanHQ::Client.new(api_type: api_type)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Perform a GET request via `Client`
|
26
|
+
#
|
27
|
+
# @param endpoint [String] API endpoint
|
28
|
+
# @param params [Hash] Query parameters
|
29
|
+
# @return [Hash, Array] The parsed API response
|
30
|
+
def get(endpoint, params: {})
|
31
|
+
formatted_params = format_params(endpoint, params)
|
32
|
+
handle_response(client.get(build_path(endpoint), formatted_params))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Perform a POST request via `Client`
|
36
|
+
#
|
37
|
+
# @param endpoint [String] API endpoint
|
38
|
+
# @param params [Hash] Request body
|
39
|
+
# @return [Hash, Array] The parsed API response
|
40
|
+
def post(endpoint, params: {})
|
41
|
+
formatted_params = format_params(endpoint, params)
|
42
|
+
handle_response(client.post(build_path(endpoint), formatted_params))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Perform a PUT request via `Client`
|
46
|
+
#
|
47
|
+
# @param endpoint [String] API endpoint
|
48
|
+
# @param params [Hash] Request body
|
49
|
+
# @return [Hash, Array] The parsed API response
|
50
|
+
def put(endpoint, params: {})
|
51
|
+
formatted_params = format_params(endpoint, params)
|
52
|
+
handle_response(client.put(build_path(endpoint), formatted_params))
|
53
|
+
end
|
54
|
+
|
55
|
+
# Perform a DELETE request via `Client`
|
56
|
+
#
|
57
|
+
# @param endpoint [String] API endpoint
|
58
|
+
# @return [Hash, Array] The parsed API response
|
59
|
+
def delete(endpoint)
|
60
|
+
formatted_params = format_params(endpoint, {})
|
61
|
+
handle_response(client.delete(build_path(endpoint), formatted_params))
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Performs an API request.
|
67
|
+
#
|
68
|
+
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
69
|
+
# @param endpoint [String] API endpoint
|
70
|
+
# @param params [Hash] Request parameters
|
71
|
+
# @return [Hash, Array] The parsed API response
|
72
|
+
# @raise [DhanHQ::Error] If an API error occurs.
|
73
|
+
def request(method, endpoint, params: {})
|
74
|
+
formatted_params = format_params(endpoint, params)
|
75
|
+
|
76
|
+
response = client.request(method, build_path(endpoint), formatted_params)
|
77
|
+
|
78
|
+
handle_response(response)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Construct the complete API URL
|
82
|
+
#
|
83
|
+
# @param endpoint [String] API endpoint
|
84
|
+
# @return [String] Full API path
|
85
|
+
def build_path(endpoint)
|
86
|
+
"#{self.class::HTTP_PATH}#{endpoint}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Format parameters based on API endpoint
|
90
|
+
def format_params(endpoint, params)
|
91
|
+
return params if marketfeed_api?(endpoint) || params.empty?
|
92
|
+
|
93
|
+
optionchain_api?(endpoint) ? titleize_keys(params) : camelize_keys(params)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Determines if the API endpoint is for Option Chain
|
97
|
+
def optionchain_api?(endpoint)
|
98
|
+
endpoint.include?("/optionchain")
|
99
|
+
end
|
100
|
+
|
101
|
+
def marketfeed_api?(endpoint)
|
102
|
+
endpoint.include?("/marketfeed")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry-validation"
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
5
|
+
require "active_support/inflector"
|
6
|
+
|
7
|
+
module DhanHQ
|
8
|
+
# Base class for resource objects
|
9
|
+
# Handles validation, attribute mapping, and response parsing
|
10
|
+
class BaseModel
|
11
|
+
# Extend & Include Modules
|
12
|
+
extend DhanHQ::APIHelper
|
13
|
+
extend DhanHQ::AttributeHelper
|
14
|
+
extend DhanHQ::ValidationHelper
|
15
|
+
extend DhanHQ::RequestHelper
|
16
|
+
extend DhanHQ::ResponseHelper
|
17
|
+
|
18
|
+
include DhanHQ::APIHelper
|
19
|
+
include DhanHQ::AttributeHelper
|
20
|
+
include DhanHQ::ValidationHelper
|
21
|
+
include DhanHQ::RequestHelper
|
22
|
+
include DhanHQ::ResponseHelper
|
23
|
+
|
24
|
+
# Attribute Accessors
|
25
|
+
attr_reader :attributes, :errors
|
26
|
+
|
27
|
+
# Initialize a new resource object
|
28
|
+
#
|
29
|
+
# @param attributes [Hash] The attributes of the resource
|
30
|
+
def initialize(attributes = {}, skip_validation: false)
|
31
|
+
@attributes = normalize_keys(attributes)
|
32
|
+
@errors = {}
|
33
|
+
|
34
|
+
validate! unless skip_validation
|
35
|
+
assign_attributes
|
36
|
+
end
|
37
|
+
|
38
|
+
# Class Methods
|
39
|
+
# Attributes set by child classes
|
40
|
+
class << self
|
41
|
+
attr_reader :defined_attributes
|
42
|
+
|
43
|
+
# Registers the set of attributes for this model
|
44
|
+
#
|
45
|
+
# @param args [Array<Symbol, String>] A list of attribute names
|
46
|
+
def attributes(*args)
|
47
|
+
@defined_attributes ||= []
|
48
|
+
@defined_attributes.concat(args.map(&:to_s))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Provide a default API type, can be overridden by child classes
|
52
|
+
#
|
53
|
+
# e.g., def self.api_type; :data_api; end
|
54
|
+
#
|
55
|
+
# or override the `api` method entirely
|
56
|
+
def api_type
|
57
|
+
:order_api
|
58
|
+
end
|
59
|
+
|
60
|
+
# Provide a shared BaseAPI instance for this model
|
61
|
+
#
|
62
|
+
# For child classes, override `api_type` or `api` if needed
|
63
|
+
def api
|
64
|
+
@api ||= BaseAPI.new(api_type: api_type)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Returns the API resource used by collection methods.
|
69
|
+
#
|
70
|
+
# Subclasses may override this to return a specialized API class.
|
71
|
+
# By default it simply returns {#api}.
|
72
|
+
def resource
|
73
|
+
api
|
74
|
+
end
|
75
|
+
|
76
|
+
# Retrieve the resource path for the API
|
77
|
+
#
|
78
|
+
# @return [String] The resource path
|
79
|
+
def resource_path
|
80
|
+
self::HTTP_PATH
|
81
|
+
end
|
82
|
+
|
83
|
+
# Every model must either override this or set a Dry::Validation contract if they need validation
|
84
|
+
#
|
85
|
+
# @return [Dry::Validation::Contract] The validation contract
|
86
|
+
def validation_contract
|
87
|
+
raise NotImplementedError, "#{name} must implement `validation_contract`"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Validate attributes before creating a new instance
|
91
|
+
def validate_attributes(attributes)
|
92
|
+
contract = validation_contract
|
93
|
+
result = contract.call(attributes)
|
94
|
+
|
95
|
+
raise ArgumentError, "Validation failed: #{result.errors.to_h}" if result.failure?
|
96
|
+
end
|
97
|
+
|
98
|
+
# == CRUD / Collection Methods
|
99
|
+
|
100
|
+
# Find all resources
|
101
|
+
#
|
102
|
+
# @return [Array<DhanHQ::BaseModel>, DhanHQ::ErrorObject] An array of resources or error object
|
103
|
+
def all
|
104
|
+
response = resource.get("")
|
105
|
+
|
106
|
+
parse_collection_response(response)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Find a resource by ID
|
110
|
+
#
|
111
|
+
# @param id [String] The ID of the resource
|
112
|
+
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
|
113
|
+
def find(id)
|
114
|
+
response = resource.get("/#{id}")
|
115
|
+
|
116
|
+
payload = response.is_a?(Array) ? response.first : response
|
117
|
+
build_from_response(payload)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Fetches records filtered by query parameters.
|
121
|
+
#
|
122
|
+
# @param params [Hash] Query parameters supported by the API.
|
123
|
+
# @return [Array<BaseModel>, BaseModel, DhanHQ::ErrorObject]
|
124
|
+
def where(params)
|
125
|
+
response = resource.get("", params: params)
|
126
|
+
build_from_response(response)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Create a new resource
|
130
|
+
#
|
131
|
+
# @param attributes [Hash] The attributes of the resource
|
132
|
+
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
|
133
|
+
def create(attributes)
|
134
|
+
# validate_params!(attributes, validation_contract)
|
135
|
+
|
136
|
+
response = resource.post("", params: attributes)
|
137
|
+
build_from_response(response)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Helper method to parse a collection response into model instances
|
141
|
+
#
|
142
|
+
# @param response [Object] The raw response from the API
|
143
|
+
# @return [Array<BaseModel>]
|
144
|
+
def parse_collection_response(response)
|
145
|
+
# Some endpoints return arrays, others might return a `[:data]` structure
|
146
|
+
return [] unless response.is_a?(Array) || (response.is_a?(Hash) && response[:data].is_a?(Array))
|
147
|
+
|
148
|
+
collection = response.is_a?(Array) ? response : response[:data]
|
149
|
+
collection.map { |record| new(record) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Instance Methods
|
154
|
+
|
155
|
+
# Update an existing resource
|
156
|
+
#
|
157
|
+
# @param attributes [Hash] Attributes to update
|
158
|
+
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject]
|
159
|
+
def update(attributes = {})
|
160
|
+
response = self.class.resource.put("/#{id}", params: attributes)
|
161
|
+
|
162
|
+
success_response?(response) ? self.class.build_from_response(response) : DhanHQ::ErrorObject.new(response)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Persists the current resource by delegating to {#create} or {#update}.
|
166
|
+
#
|
167
|
+
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject, false]
|
168
|
+
def save
|
169
|
+
new_record? ? self.class.create(attributes) : update(attributes)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Same as {#save} but raises {DhanHQ::Error} when persistence fails.
|
173
|
+
#
|
174
|
+
# @return [DhanHQ::BaseModel]
|
175
|
+
# @raise [DhanHQ::Error] When the record cannot be saved.
|
176
|
+
def save!
|
177
|
+
result = save
|
178
|
+
return result unless result == false || result.nil? || result.is_a?(DhanHQ::ErrorObject)
|
179
|
+
|
180
|
+
error_details =
|
181
|
+
if result.is_a?(DhanHQ::ErrorObject)
|
182
|
+
result.errors
|
183
|
+
elsif @errors && !@errors.empty?
|
184
|
+
@errors
|
185
|
+
else
|
186
|
+
"Unknown error"
|
187
|
+
end
|
188
|
+
|
189
|
+
raise DhanHQ::Error, "Failed to save the record: #{error_details}"
|
190
|
+
end
|
191
|
+
|
192
|
+
# Delete the resource
|
193
|
+
#
|
194
|
+
# @return [Boolean] True if deletion was successful
|
195
|
+
# Deletes the resource from the remote API.
|
196
|
+
#
|
197
|
+
# @return [Boolean] True when the server confirms deletion.
|
198
|
+
def delete
|
199
|
+
response = self.class.resource.delete("/#{id}")
|
200
|
+
success_response?(response)
|
201
|
+
rescue StandardError
|
202
|
+
false
|
203
|
+
end
|
204
|
+
|
205
|
+
# Alias for {#delete} maintained for ActiveModel familiarity.
|
206
|
+
#
|
207
|
+
# @return [Boolean]
|
208
|
+
def destroy
|
209
|
+
response = self.class.resource.delete("/#{id}")
|
210
|
+
success_response?(response)
|
211
|
+
rescue StandardError
|
212
|
+
false
|
213
|
+
end
|
214
|
+
|
215
|
+
def persisted?
|
216
|
+
!!id
|
217
|
+
end
|
218
|
+
|
219
|
+
def new_record?
|
220
|
+
!persisted?
|
221
|
+
end
|
222
|
+
|
223
|
+
# Format request parameters before sending to API
|
224
|
+
#
|
225
|
+
# @return [Hash] The camelCased attributes
|
226
|
+
def to_request_params
|
227
|
+
optionchain_api? ? titleize_keys(@attributes) : camelize_keys(@attributes)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Identifier inferred from the loaded attributes.
|
231
|
+
#
|
232
|
+
# @return [String, Integer, nil]
|
233
|
+
def id
|
234
|
+
@attributes[:id] || @attributes[:order_id] || @attributes[:security_id]
|
235
|
+
end
|
236
|
+
|
237
|
+
# Dynamically assign attributes as methods
|
238
|
+
def assign_attributes
|
239
|
+
self.class.defined_attributes&.each do |attr|
|
240
|
+
instance_variable_set(:"@#{attr}", @attributes[attr])
|
241
|
+
define_singleton_method(attr) { instance_variable_get(:"@#{attr}") }
|
242
|
+
define_singleton_method(attr.to_s.camelize(:lower)) { instance_variable_get(:"@#{attr}") }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def optionchain_api?
|
247
|
+
self.class.name.include?("OptionChain")
|
248
|
+
end
|
249
|
+
|
250
|
+
# Validate attributes using contract
|
251
|
+
def valid?
|
252
|
+
contract_class = respond_to?(:validation_contract) ? validation_contract : self.class.validation_contract
|
253
|
+
return true unless contract_class
|
254
|
+
|
255
|
+
contract = contract_class.is_a?(Class) ? contract_class.new : contract_class
|
256
|
+
result = contract.call(@attributes)
|
257
|
+
|
258
|
+
if result.failure?
|
259
|
+
@errors = result.errors.to_h
|
260
|
+
return false
|
261
|
+
end
|
262
|
+
|
263
|
+
true
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Base wrapper exposing RESTful helpers used by resource classes.
|
5
|
+
class BaseResource < BaseAPI
|
6
|
+
def initialize(api_type: self.class::API_TYPE)
|
7
|
+
super(api_type: api_type) # rubocop:disable Style/SuperArguments
|
8
|
+
end
|
9
|
+
|
10
|
+
# Fetches all records for the resource.
|
11
|
+
#
|
12
|
+
# @return [Array<Hash>, Hash]
|
13
|
+
def all
|
14
|
+
get(self.class::HTTP_PATH)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Retrieves a single resource by identifier.
|
18
|
+
#
|
19
|
+
# @param id [String, Integer]
|
20
|
+
# @return [Hash]
|
21
|
+
def find(id)
|
22
|
+
get("#{self.class::HTTP_PATH}/#{id}")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a new resource instance.
|
26
|
+
#
|
27
|
+
# @param params [Hash]
|
28
|
+
# @return [Hash]
|
29
|
+
def create(params)
|
30
|
+
post(self.class::HTTP_PATH, params: params)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Updates an existing resource.
|
34
|
+
#
|
35
|
+
# @param id [String, Integer]
|
36
|
+
# @param params [Hash]
|
37
|
+
# @return [Hash]
|
38
|
+
def update(id, params)
|
39
|
+
put("#{self.class::HTTP_PATH}/#{id}", params: params)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Deletes a resource by identifier.
|
43
|
+
#
|
44
|
+
# @param id [String, Integer]
|
45
|
+
# @return [Hash]
|
46
|
+
def delete(id)
|
47
|
+
super("#{self.class::HTTP_PATH}/#{id}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Provides a minimal shim for surfacing validation and runtime errors.
|
5
|
+
class ErrorHandler
|
6
|
+
# Normalises the exception raised for various error types.
|
7
|
+
#
|
8
|
+
# @param error [Dry::Validation::Result, StandardError]
|
9
|
+
# @raise [RuntimeError]
|
10
|
+
def self.handle(error)
|
11
|
+
case error
|
12
|
+
when Dry::Validation::Result
|
13
|
+
raise "Validation Error: #{error.errors.to_h}"
|
14
|
+
else
|
15
|
+
raise "Error: #{error.message}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Wrapper class for API error responses
|
5
|
+
class ErrorObject
|
6
|
+
# @return [Hash] Raw error response
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
# Initialize a new ErrorObject
|
10
|
+
#
|
11
|
+
# @param response [Hash] Parsed API response
|
12
|
+
def initialize(response)
|
13
|
+
@response =
|
14
|
+
if response.is_a?(Hash)
|
15
|
+
response.with_indifferent_access
|
16
|
+
else
|
17
|
+
{ message: response.to_s }.with_indifferent_access
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Always returns false to mimic success? interface on resources
|
22
|
+
#
|
23
|
+
# @return [Boolean]
|
24
|
+
def success?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Extracts the error message from the response
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
def message
|
32
|
+
response[:errorMessage] || response[:message] || response[:error] || "Unknown error"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Error code if present
|
36
|
+
#
|
37
|
+
# @return [String, nil]
|
38
|
+
def code
|
39
|
+
response[:errorCode]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Alias for the raw response hash
|
43
|
+
#
|
44
|
+
# @return [Hash]
|
45
|
+
def errors
|
46
|
+
response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Base error class for all DhanHQ API errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
# Authentication and access errors
|
8
|
+
# DH-901
|
9
|
+
class InvalidAuthenticationError < Error; end
|
10
|
+
# DH-902
|
11
|
+
class InvalidAccessError < Error; end
|
12
|
+
# DH-903
|
13
|
+
class UserAccountError < Error; end
|
14
|
+
# DH-808
|
15
|
+
class AuthenticationFailedError < Error; end
|
16
|
+
# DH-807, DH-809
|
17
|
+
class InvalidTokenError < Error; end
|
18
|
+
# DH-810
|
19
|
+
class InvalidClientIDError < Error; end
|
20
|
+
|
21
|
+
# Rate limits and input validation errors
|
22
|
+
# DH-904, 805
|
23
|
+
class RateLimitError < Error; end
|
24
|
+
# DH-905
|
25
|
+
class InputExceptionError < Error; end
|
26
|
+
# DH-811, DH-812, DH-813, DH-814
|
27
|
+
class InvalidRequestError < Error; end
|
28
|
+
|
29
|
+
# Order and market data errors
|
30
|
+
class OrderError < Error; end
|
31
|
+
# Raised when the API signals an issue with the requested data payload.
|
32
|
+
class DataError < Error; end
|
33
|
+
|
34
|
+
# Server and network-related errors
|
35
|
+
# DH-908, 800
|
36
|
+
class InternalServerError < Error; end
|
37
|
+
# DH-1111
|
38
|
+
class NoHoldingsError < Error; end
|
39
|
+
# DH-909
|
40
|
+
class NetworkError < Error; end
|
41
|
+
# DH-910
|
42
|
+
class OtherError < Error; end
|
43
|
+
# 404
|
44
|
+
class NotFoundError < Error; end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
# Helper mixin offering response validation behaviour for API wrappers.
|
5
|
+
module APIHelper
|
6
|
+
# Ensures the response is a structured payload before returning it.
|
7
|
+
#
|
8
|
+
# @param response [Hash, Array]
|
9
|
+
# @return [Hash, Array]
|
10
|
+
# @raise [DhanHQ::Error] When an unexpected payload type is received.
|
11
|
+
def handle_response(response)
|
12
|
+
return response if response.is_a?(Array) || response.is_a?(Hash)
|
13
|
+
|
14
|
+
raise DhanHQ::Error, "Unexpected API response format"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|