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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Represents a single row/entry in the Ledger.
7
+ # Ledger data typically returns an array of these objects.
8
+ class LedgerEntry < BaseModel
9
+ # The endpoint is /v2/ledger?from-date=...&to-date=...
10
+ # So we may define a resource path or rely on the Statements resource.
11
+ HTTP_PATH = "/v2/ledger"
12
+
13
+ # Typical fields from API docs
14
+ attributes :dhan_client_id, :narration, :voucherdate, :exchange,
15
+ :voucherdesc, :vouchernumber, :debit, :credit, :runbal
16
+
17
+ class << self
18
+ ##
19
+ # Provides a **shared instance** of the `Statements` resource.
20
+ #
21
+ # @return [DhanHQ::Resources::Statements]
22
+ def resource
23
+ @resource ||= DhanHQ::Resources::Statements.new
24
+ end
25
+
26
+ ##
27
+ # Fetch ledger entries for the given date range.
28
+ #
29
+ # @param from_date [String] e.g. "2023-01-01"
30
+ # @param to_date [String] e.g. "2023-01-31"
31
+ # @return [Array<LedgerEntry>]
32
+ def all(from_date:, to_date:)
33
+ # The resource call returns an Array<Hash>, according to the docs.
34
+ response = resource.ledger(from_date: from_date, to_date: to_date)
35
+
36
+ return [] unless response.is_a?(Array)
37
+
38
+ response.map do |entry|
39
+ new(entry, skip_validation: true)
40
+ end
41
+ end
42
+ end
43
+
44
+ # Optional: you can override #to_h or #inspect if you want a custom representation
45
+ def to_h
46
+ {
47
+ dhan_client_id: dhan_client_id,
48
+ narration: narration,
49
+ voucherdate: voucherdate,
50
+ exchange: exchange,
51
+ voucherdesc: voucherdesc,
52
+ vouchernumber: vouchernumber,
53
+ debit: debit,
54
+ credit: credit,
55
+ runbal: runbal
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model for the on-demand margin calculator response.
6
+ class Margin < BaseModel
7
+ # Base path used to invoke the calculator.
8
+ HTTP_PATH = "/v2/margincalculator"
9
+
10
+ attr_reader :total_margin, :span_margin, :exposure_margin, :available_balance,
11
+ :variable_margin, :insufficient_balance, :brokerage, :leverage
12
+
13
+ class << self
14
+ ##
15
+ # Provides a **shared instance** of the `MarginCalculator` resource.
16
+ #
17
+ # @return [DhanHQ::Resources::MarginCalculator]
18
+ def resource
19
+ @resource ||= DhanHQ::Resources::MarginCalculator.new
20
+ end
21
+
22
+ ##
23
+ # Calculate margin requirements for an order.
24
+ #
25
+ # @param params [Hash] Request parameters for margin calculation.
26
+ # @return [Margin]
27
+ def calculate(params)
28
+ formatted_params = camelize_keys(params)
29
+ validate_params!(formatted_params, DhanHQ::Contracts::MarginCalculatorContract)
30
+
31
+ response = resource.calculate(formatted_params)
32
+ new(response, skip_validation: true)
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Convert model attributes to a hash.
38
+ #
39
+ # @return [Hash] Hash representation of the Margin model.
40
+ def to_h
41
+ {
42
+ total_margin: total_margin,
43
+ span_margin: span_margin,
44
+ exposure_margin: exposure_margin,
45
+ available_balance: available_balance,
46
+ variable_margin: variable_margin,
47
+ insufficient_balance: insufficient_balance,
48
+ brokerage: brokerage,
49
+ leverage: leverage
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Lightweight wrapper exposing market feed resources.
6
+ class MarketFeed < BaseModel
7
+ class << self
8
+ # Fetches last traded price snapshots.
9
+ #
10
+ # @param params [Hash]
11
+ # @return [Hash]
12
+ def ltp(params)
13
+ resource.ltp(params)
14
+ end
15
+
16
+ # Fetches OHLC data for the requested instruments.
17
+ #
18
+ # @param params [Hash]
19
+ # @return [Hash]
20
+ def ohlc(params)
21
+ resource.ohlc(params)
22
+ end
23
+
24
+ # Fetches full quote depth and analytics.
25
+ #
26
+ # @param params [Hash]
27
+ # @return [Hash]
28
+ def quote(params)
29
+ resource.quote(params)
30
+ end
31
+
32
+ # Shared market feed resource instance.
33
+ #
34
+ # @return [DhanHQ::Resources::MarketFeed]
35
+ def resource
36
+ @resource ||= DhanHQ::Resources::MarketFeed.new
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../contracts/option_chain_contract"
4
+
5
+ module DhanHQ
6
+ module Models
7
+ # Model for fetching and filtering option chain snapshots.
8
+ class OptionChain < BaseModel
9
+ attr_reader :underlying_scrip, :underlying_seg, :expiry, :last_price, :option_data
10
+
11
+ class << self
12
+ # Shared resource for option chain operations.
13
+ #
14
+ # @return [DhanHQ::Resources::OptionChain]
15
+ def resource
16
+ @resource ||= DhanHQ::Resources::OptionChain.new
17
+ end
18
+
19
+ # Fetch the entire option chain for an instrument
20
+ #
21
+ # @param params [Hash] The request parameters (snake_case format)
22
+ # @return [HashWithIndifferentAccess] The filtered option chain data
23
+ def fetch(params)
24
+ validate_params!(params, DhanHQ::Contracts::OptionChainContract)
25
+
26
+ response = resource.fetch(params)
27
+ return {}.with_indifferent_access unless response[:status] == "success"
28
+
29
+ filter_valid_strikes(response[:data]).with_indifferent_access
30
+ end
31
+
32
+ # Fetch the expiry list of an underlying security
33
+ #
34
+ # @param params [Hash] The request parameters (snake_case format)
35
+ # @return [Array<String>] The list of expiry dates
36
+ def fetch_expiry_list(params)
37
+ response = resource.expirylist(params)
38
+ response[:status] == "success" ? response[:data] : []
39
+ end
40
+
41
+ private
42
+
43
+ # **Filters valid strikes where `ce` or `pe` has `last_price > 0` and keeps strike prices as-is**
44
+ #
45
+ # @param data [Hash] The API response data
46
+ # @return [Hash] The filtered option chain data with original strike price keys
47
+ def filter_valid_strikes(data)
48
+ return {} unless data.is_a?(Hash) && data.key?(:oc)
49
+
50
+ filtered_oc = data[:oc].each_with_object({}) do |(strike_price, strike_data), result|
51
+ ce_last_price = strike_data.dig("ce", "last_price").to_f
52
+ pe_last_price = strike_data.dig("pe", "last_price").to_f
53
+
54
+ # Only keep strikes where at least one of CE or PE has a valid last_price
55
+ result[strike_price] = strike_data if ce_last_price.positive? || pe_last_price.positive?
56
+ end
57
+
58
+ data.merge(oc: filtered_oc)
59
+ end
60
+
61
+ # Validation contract for option chain
62
+ #
63
+ # @return [DhanHQ::Contracts::OptionChainContract]
64
+ def validation_contract
65
+ DhanHQ::Contracts::OptionChainContract.new
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # Validation contract for option chain
72
+ #
73
+ # @return [DhanHQ::Contracts::OptionChainContract]
74
+ def validation_contract
75
+ DhanHQ::Contracts::OptionChainContract.new
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../contracts/place_order_contract"
4
+ require_relative "../contracts/modify_order_contract"
5
+
6
+ module DhanHQ
7
+ # ActiveRecord-style models built on top of the REST resources.
8
+ module Models
9
+ # Representation of an order as returned by the REST APIs.
10
+ class Order < BaseModel
11
+ # Attributes eligible for modification requests.
12
+ MODIFIABLE_FIELDS = %i[
13
+ dhan_client_id
14
+ order_id
15
+ order_type
16
+ quantity
17
+ price
18
+ trigger_price
19
+ disclosed_quantity
20
+ validity
21
+ leg_name
22
+ ].freeze
23
+
24
+ attr_reader :order_id, :order_status
25
+
26
+ # Define attributes that are part of an order
27
+ attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
28
+ :transaction_type, :exchange_segment, :product_type, :order_type,
29
+ :validity, :trading_symbol, :security_id, :quantity,
30
+ :disclosed_quantity, :price, :trigger_price, :after_market_order,
31
+ :bo_profit_value, :bo_stop_loss_value, :leg_name, :create_time,
32
+ :update_time, :exchange_time, :drv_expiry_date, :drv_option_type,
33
+ :drv_strike_price, :oms_error_code, :oms_error_description, :algo_id,
34
+ :remaining_quantity, :average_traded_price, :filled_qty
35
+
36
+ class << self
37
+ ##
38
+ # Provides a **shared instance** of the `Orders` resource
39
+ #
40
+ # @return [DhanHQ::Resources::Orders]
41
+ def resource
42
+ @resource ||= DhanHQ::Resources::Orders.new
43
+ end
44
+
45
+ ##
46
+ # Fetch all orders for the day.
47
+ #
48
+ # @return [Array<Order>]
49
+ def all
50
+ response = resource.all
51
+ return [] unless response.is_a?(Array)
52
+
53
+ response.map { |order| new(order, skip_validation: true) }
54
+ end
55
+
56
+ ##
57
+ # Fetch a specific order by ID.
58
+ #
59
+ # @param order_id [String]
60
+ # @return [Order, nil]
61
+ def find(order_id)
62
+ response = resource.find(order_id)
63
+ return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
64
+
65
+ order_data = response.is_a?(Array) ? response.first : response
66
+ new(order_data, skip_validation: true)
67
+ end
68
+
69
+ ##
70
+ # Fetch a specific order by correlation ID.
71
+ #
72
+ # @param correlation_id [String]
73
+ # @return [Order, nil]
74
+ def find_by_correlation(correlation_id)
75
+ response = resource.by_correlation(correlation_id)
76
+ return nil unless response[:status] == "success"
77
+
78
+ new(response, skip_validation: true)
79
+ end
80
+
81
+ # Place a new order
82
+ #
83
+ # @param params [Hash] Order parameters
84
+ # @return [Order]
85
+ def place(params)
86
+ normalized_params = snake_case(params)
87
+ validate_params!(normalized_params, DhanHQ::Contracts::PlaceOrderContract)
88
+
89
+ response = resource.create(camelize_keys(normalized_params))
90
+ return nil unless response.is_a?(Hash) && response["orderId"]
91
+
92
+ # Fetch the complete order details
93
+ find(response["orderId"])
94
+ end
95
+
96
+ ##
97
+ # AR-like create: new => valid? => save => resource.create
98
+ # But we can also define a class method if we want direct:
99
+ # Order.create(order_params)
100
+ #
101
+ # For the typical usage "Order.new(...).save", we rely on #save below.
102
+ def create(params)
103
+ order = new(params) # build it
104
+ return order unless order.valid? # run place order contract?
105
+
106
+ order.save # calls resource create or update
107
+ order
108
+ end
109
+ end
110
+
111
+ # Modify the order while preserving existing attributes
112
+ #
113
+ # @param new_params [Hash]
114
+ # @return [Order, nil]
115
+ def modify(new_params)
116
+ raise "Order ID is required to modify an order" unless id
117
+
118
+ base_payload = attributes.merge(new_params)
119
+ normalized_payload = snake_case(base_payload).merge(order_id: id)
120
+ filtered_payload = normalized_payload.each_with_object({}) do |(key, value), memo|
121
+ symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
122
+ memo[symbolized_key] = value if MODIFIABLE_FIELDS.include?(symbolized_key)
123
+ end
124
+ filtered_payload[:order_id] ||= id
125
+ filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id]
126
+
127
+ cleaned_payload = filtered_payload.compact
128
+ formatted_payload = camelize_keys(cleaned_payload)
129
+ validate_params!(formatted_payload, DhanHQ::Contracts::ModifyOrderContract)
130
+
131
+ response = self.class.resource.update(id, formatted_payload)
132
+ response = response.with_indifferent_access if response.respond_to?(:with_indifferent_access)
133
+
134
+ return DhanHQ::ErrorObject.new(response) unless success_response?(response)
135
+
136
+ @attributes.merge!(normalize_keys(response))
137
+ assign_attributes
138
+ self
139
+ end
140
+
141
+ # Cancel the order
142
+ #
143
+ # @return [Boolean]
144
+ def cancel
145
+ raise "Order ID is required to cancel an order" unless id
146
+
147
+ response = self.class.resource.cancel(id)
148
+ response["orderStatus"] == "CANCELLED"
149
+ end
150
+
151
+ # Fetch the latest details of the order
152
+ #
153
+ # @return [Order, nil]
154
+ def refresh
155
+ raise "Order ID is required to refresh an order" unless id
156
+
157
+ self.class.find(id)
158
+ end
159
+
160
+ ##
161
+ # This is how we figure out if it's an existing record or not:
162
+ def new_record?
163
+ order_id.nil? || order_id.to_s.empty?
164
+ end
165
+
166
+ ##
167
+ # The ID used for resource calls
168
+ def id
169
+ order_id
170
+ end
171
+
172
+ ##
173
+ # Save: If new_record?, do resource.create
174
+ # else resource.update
175
+ def save
176
+ return false unless valid?
177
+
178
+ if new_record?
179
+ # PLACE ORDER
180
+ response = self.class.resource.create(to_request_params)
181
+ if success_response?(response) && response["orderId"]
182
+ @attributes.merge!(normalize_keys(response))
183
+ assign_attributes
184
+ true
185
+ else
186
+ # maybe store errors?
187
+ false
188
+ end
189
+ else
190
+ # MODIFY ORDER
191
+ response = self.class.resource.update(id, to_request_params)
192
+ if success_response?(response) && response["orderStatus"]
193
+ @attributes.merge!(normalize_keys(response))
194
+ assign_attributes
195
+ true
196
+ else
197
+ false
198
+ end
199
+ end
200
+ end
201
+
202
+ ##
203
+ # Cancel => calls resource.delete
204
+ def destroy
205
+ return false if new_record?
206
+
207
+ response = self.class.resource.delete(id)
208
+ if success_response?(response) && response["orderStatus"] == "CANCELLED"
209
+ @attributes[:order_status] = "CANCELLED"
210
+ true
211
+ else
212
+ false
213
+ end
214
+ end
215
+ alias delete destroy
216
+
217
+ ##
218
+ # Slicing (optional)
219
+ # If you want an AR approach:
220
+ def slice_order(params)
221
+ raise "Order ID is required to slice an order" unless id
222
+
223
+ base_payload = params.merge(order_id: id)
224
+ formatted_payload = camelize_keys(base_payload)
225
+
226
+ validate_params!(formatted_payload, DhanHQ::Contracts::SliceOrderContract)
227
+
228
+ self.class.resource.slicing(formatted_payload)
229
+ end
230
+
231
+ ##
232
+ # Because we have two separate contracts: place vs. modify
233
+ # We can do something like:
234
+ def validation_contract
235
+ new_record? ? DhanHQ::Contracts::PlaceOrderContract.new : DhanHQ::Contracts::ModifyOrderContract.new
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model representing an intraday or carry-forward position snapshot.
6
+ class Position < BaseModel
7
+ # Base path used by the positions resource.
8
+ HTTP_PATH = "/v2/positions"
9
+
10
+ attributes :dhan_client_id, :trading_symbol, :security_id, :position_type, :exchange_segment,
11
+ :product_type, :buy_avg, :buy_qty, :cost_price, :sell_avg, :sell_qty,
12
+ :net_qty, :realized_profit, :unrealized_profit, :rbi_reference_rate, :multiplier,
13
+ :carry_forward_buy_qty, :carry_forward_sell_qty, :carry_forward_buy_value,
14
+ :carry_forward_sell_value, :day_buy_qty, :day_sell_qty, :day_buy_value,
15
+ :day_sell_value, :drv_expiry_date, :drv_option_type, :drv_strike_price,
16
+ :cross_currency
17
+
18
+ class << self
19
+ ##
20
+ # Provides a **shared instance** of the `Positions` resource.
21
+ #
22
+ # @return [DhanHQ::Resources::Positions]
23
+ def resource
24
+ @resource ||= DhanHQ::Resources::Positions.new
25
+ end
26
+
27
+ ##
28
+ # Fetch all positions for the day.
29
+ #
30
+ # @return [Array<Position>]
31
+ def all
32
+ response = resource.all
33
+ return [] unless response.is_a?(Array)
34
+
35
+ response.map do |position|
36
+ new(snake_case(position), skip_validation: true)
37
+ end
38
+ end
39
+
40
+ # Filters the position list down to non-closed entries.
41
+ #
42
+ # @return [Array<Position>]
43
+ def active
44
+ all.reject { |position| position.position_type == "CLOSED" }
45
+ end
46
+
47
+ # Convert an existing position (intraday <-> delivery)
48
+ # @param params [Hash] parameters as required by the API
49
+ # @return [Hash, DhanHQ::ErrorObject]
50
+ def convert(params)
51
+ formatted_params = camelize_keys(params)
52
+ validate_params!(formatted_params, DhanHQ::Contracts::PositionConversionContract)
53
+
54
+ response = resource.convert(formatted_params)
55
+ success_response?(response) ? response : DhanHQ::ErrorObject.new(response)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Ruby wrapper around the `/v2/profile` endpoint. Provides typed accessors
7
+ # and snake_case keys while leaving the underlying response untouched.
8
+ class Profile < BaseModel
9
+ # Base path for profile retrieval.
10
+ HTTP_PATH = "/v2/profile"
11
+
12
+ attributes :dhan_client_id, :token_validity, :active_segment, :ddpi,
13
+ :mtf, :data_plan, :data_validity
14
+
15
+ class << self
16
+ ##
17
+ # Provides a shared instance of the profile resource.
18
+ #
19
+ # @return [DhanHQ::Resources::Profile]
20
+ def resource
21
+ @resource ||= DhanHQ::Resources::Profile.new
22
+ end
23
+
24
+ ##
25
+ # Fetch the authenticated user's profile details.
26
+ #
27
+ # @return [DhanHQ::Models::Profile, nil]
28
+ def fetch
29
+ response = resource.fetch
30
+ return nil unless response.is_a?(Hash)
31
+
32
+ new(response, skip_validation: true)
33
+ end
34
+ end
35
+
36
+ # Profile responses are informational and not validated locally.
37
+ #
38
+ # @return [nil]
39
+ def validation_contract
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Model wrapping multi-leg super order payloads.
6
+ class SuperOrder < BaseModel
7
+ attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
8
+ :transaction_type, :exchange_segment, :product_type, :order_type,
9
+ :validity, :trading_symbol, :security_id, :quantity,
10
+ :remaining_quantity, :ltp, :price, :after_market_order,
11
+ :leg_name, :exchange_order_id, :create_time, :update_time,
12
+ :exchange_time, :oms_error_description, :average_traded_price,
13
+ :filled_qty, :leg_details, :target_price, :stop_loss_price,
14
+ :trailing_jump
15
+
16
+ class << self
17
+ # Shared resource instance used for API calls.
18
+ #
19
+ # @return [DhanHQ::Resources::SuperOrders]
20
+ def resource
21
+ @resource ||= DhanHQ::Resources::SuperOrders.new
22
+ end
23
+
24
+ # Fetches all configured super orders.
25
+ #
26
+ # @return [Array<SuperOrder>]
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
+ # Creates a new super order with the provided legs.
35
+ #
36
+ # @param params [Hash]
37
+ # @return [SuperOrder, nil]
38
+ def create(params)
39
+ response = resource.create(params)
40
+ return nil unless response.is_a?(Hash) && response["orderId"]
41
+
42
+ new(order_id: response["orderId"], order_status: response["orderStatus"], skip_validation: true)
43
+ end
44
+ end
45
+
46
+ # Updates the order legs for an existing super order.
47
+ #
48
+ # @param new_params [Hash]
49
+ # @return [Boolean]
50
+ def modify(new_params)
51
+ raise "Order ID is required to modify a super order" unless id
52
+
53
+ response = self.class.resource.update(id, new_params)
54
+ response["orderId"] == id
55
+ end
56
+
57
+ # Cancels a specific leg (or the entry leg by default).
58
+ #
59
+ # @param leg_name [String]
60
+ # @return [Boolean]
61
+ def cancel(leg_name = "ENTRY_LEG")
62
+ raise "Order ID is required to cancel a super order" unless id
63
+
64
+ response = self.class.resource.cancel(id, leg_name)
65
+ response["orderStatus"] == "CANCELLED"
66
+ end
67
+ end
68
+ end
69
+ end