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,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Enumerations and helper lookups used across the REST and WebSocket clients.
5
+ module Constants
6
+ # Valid transaction directions accepted by order placement APIs.
7
+ TRANSACTION_TYPES = %w[BUY SELL].freeze
8
+
9
+ # Supported exchange segments for security lookups and subscription APIs.
10
+ EXCHANGE_SEGMENTS = %w[
11
+ NSE_EQ
12
+ NSE_FNO
13
+ NSE_CURRENCY
14
+ BSE_EQ
15
+ BSE_FNO
16
+ BSE_CURRENCY
17
+ MCX_COMM
18
+ IDX_I
19
+ ].freeze
20
+
21
+ # Security instrument kinds returned in instrument master downloads.
22
+ INSTRUMENTS = %w[
23
+ INDEX
24
+ FUTIDX
25
+ OPTIDX
26
+ EQUITY
27
+ FUTSTK
28
+ OPTSTK
29
+ FUTCOM
30
+ OPTFUT
31
+ FUTCUR
32
+ OPTCUR
33
+ ].freeze
34
+
35
+ # Product types that can be used while placing or modifying orders.
36
+ PRODUCT_TYPES = %w[
37
+ CNC
38
+ INTRADAY
39
+ MARGIN
40
+ MTF
41
+ CO
42
+ BO
43
+ ].freeze
44
+
45
+ # Order execution types supported by the platform.
46
+ ORDER_TYPES = %w[
47
+ LIMIT
48
+ MARKET
49
+ STOP_LOSS
50
+ STOP_LOSS_MARKET
51
+ ].freeze
52
+
53
+ # Order validity flags supported by the trading APIs.
54
+ VALIDITY_TYPES = %w[DAY IOC].freeze
55
+
56
+ # Permitted after-market order submission windows.
57
+ AMO_TIMINGS = %w[
58
+ OPEN
59
+ OPEN_30
60
+ OPEN_60
61
+ PRE_OPEN
62
+ ].freeze
63
+
64
+ # Status values returned when querying order lifecycle events.
65
+ ORDER_STATUSES = %w[
66
+ TRANSIT
67
+ PENDING
68
+ REJECTED
69
+ CANCELLED
70
+ PART_TRADED
71
+ TRADED
72
+ EXPIRED
73
+ MODIFIED
74
+ TRIGGERED
75
+ ].freeze
76
+
77
+ # Exchange aliases used when building subscription payloads.
78
+ NSE = "NSE_EQ"
79
+ # Bombay Stock Exchange equities segment alias.
80
+ BSE = "BSE_EQ"
81
+ # Currency segment alias.
82
+ CUR = "NSE_CURRENCY"
83
+ # Multi Commodity Exchange segment alias.
84
+ MCX = "MCX_COMM"
85
+ # F&O segment alias.
86
+ FNO = "NSE_FNO"
87
+ # National Stock Exchange futures & options segment alias.
88
+ NSE_FNO = "NSE_FNO"
89
+ # Bombay Stock Exchange futures & options segment alias.
90
+ BSE_FNO = "BSE_FNO"
91
+ # Broad index segment alias.
92
+ INDEX = "IDX_I"
93
+
94
+ # Segments that support option instruments.
95
+ OPTION_SEGMENTS = [NSE, BSE, CUR, MCX, FNO, NSE_FNO, BSE_FNO, INDEX].freeze
96
+
97
+ # Canonical buy transaction label.
98
+ BUY = "BUY"
99
+ # Canonical sell transaction label.
100
+ SELL = "SELL"
101
+
102
+ # Cash-and-carry product identifier.
103
+ CNC = "CNC"
104
+ # Intraday margin product identifier.
105
+ INTRA = "INTRADAY"
106
+ # Carry-forward margin product identifier.
107
+ MARGIN = "MARGIN"
108
+ # Cover order product identifier.
109
+ CO = "CO"
110
+ # Bracket order product identifier.
111
+ BO = "BO"
112
+ # Margin trading funding identifier.
113
+ MTF = "MTF"
114
+
115
+ # Limit price order type.
116
+ LIMIT = "LIMIT"
117
+ # Market order type.
118
+ MARKET = "MARKET"
119
+ # Stop-loss limit order type.
120
+ SL = "STOP_LOSS"
121
+ # Stop-loss market order type.
122
+ SLM = "STOP_LOSS_MARKET"
123
+
124
+ # Good-for-day validity flag.
125
+ DAY = "DAY"
126
+ # Immediate-or-cancel validity flag.
127
+ IOC = "IOC"
128
+
129
+ # Download URL for the compact instrument master CSV.
130
+ COMPACT_CSV_URL = "https://images.dhan.co/api-data/api-scrip-master.csv"
131
+ # Download URL for the detailed instrument master CSV.
132
+ DETAILED_CSV_URL = "https://images.dhan.co/api-data/api-scrip-master-detailed.csv"
133
+
134
+ # API routes that require a `client-id` header in addition to the access token.
135
+ DATA_API_PATHS = %w[
136
+ /v2/marketfeed/ltp
137
+ /v2/marketfeed/ohlc
138
+ /v2/marketfeed/quote
139
+ /v2/optionchain
140
+ /v2/optionchain/expirylist
141
+ ].freeze
142
+
143
+ # Mapping of DhanHQ error codes to SDK error classes for consistent exception handling.
144
+ DHAN_ERROR_MAPPING = {
145
+ "DH-901" => DhanHQ::InvalidAuthenticationError,
146
+ "DH-902" => DhanHQ::InvalidAccessError,
147
+ "DH-903" => DhanHQ::UserAccountError,
148
+ "DH-904" => DhanHQ::RateLimitError,
149
+ "DH-905" => DhanHQ::InputExceptionError,
150
+ "DH-906" => DhanHQ::OrderError,
151
+ "DH-907" => DhanHQ::DataError,
152
+ "DH-908" => DhanHQ::InternalServerError,
153
+ "DH-1111" => DhanHQ::NoHoldingsError,
154
+ "DH-909" => DhanHQ::NetworkError,
155
+ "DH-910" => DhanHQ::OtherError,
156
+ "800" => DhanHQ::InternalServerError,
157
+ "804" => DhanHQ::Error, # Too many instruments
158
+ "805" => DhanHQ::RateLimitError, # Too many requests
159
+ "806" => DhanHQ::DataError, # Data API not subscribed
160
+ "807" => DhanHQ::InvalidTokenError, # Token expired
161
+ "808" => DhanHQ::AuthenticationFailedError, # Auth failed
162
+ "809" => DhanHQ::InvalidTokenError, # Invalid token
163
+ "810" => DhanHQ::InvalidClientIDError, # Invalid Client ID
164
+ "811" => DhanHQ::InvalidRequestError, # Invalid expiry date
165
+ "812" => DhanHQ::InvalidRequestError, # Invalid date format
166
+ "813" => DhanHQ::InvalidRequestError, # Invalid security ID
167
+ "814" => DhanHQ::InvalidRequestError # Invalid request
168
+ }.freeze
169
+ end
170
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-validation"
4
+ require_relative "../constants"
5
+
6
+ module DhanHQ
7
+ # Namespace housing Dry::Validation contracts for request payload validation.
8
+ module Contracts
9
+ # Base contract that includes shared logic and constants.
10
+ class BaseContract < Dry::Validation::Contract
11
+ # Include constants to make them accessible in all derived contracts
12
+ include DhanHQ::Constants
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validates payloads for the historical data endpoints.
6
+ class HistoricalDataContract < Dry::Validation::Contract
7
+ include DhanHQ::Constants
8
+
9
+ params do
10
+ # Common required fields
11
+ required(:security_id).filled(:string)
12
+ required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
13
+ required(:instrument).filled(:string, included_in?: INSTRUMENTS)
14
+
15
+ # Date range required for both Daily & Intraday
16
+ required(:from_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}\z/)
17
+ required(:to_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}\z/)
18
+
19
+ # Optional fields
20
+ optional(:expiry_code).maybe(:integer, included_in?: [0, 1, 2])
21
+
22
+ # For intraday, the user can supply an "interval"
23
+ # (valid: 1, 5, 15, 25, 60)
24
+ optional(:interval).maybe(:string, included_in?: %w[1 5 15 25 60])
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validates requests sent to the margin calculator endpoint.
6
+ class MarginCalculatorContract < Dry::Validation::Contract
7
+ params do
8
+ required(:dhanClientId).filled(:string)
9
+ required(:exchangeSegment).filled(:string, included_in?: %w[NSE_EQ NSE_FNO BSE_EQ])
10
+ required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
11
+ required(:quantity).filled(:integer, gt?: 0)
12
+ required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF CO BO])
13
+ required(:securityId).filled(:string)
14
+ required(:price).filled(:float, gt?: 0)
15
+ optional(:triggerPrice).maybe(:float)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validation contract for modifying an existing order via Dhanhq's API.
6
+ #
7
+ # This contract validates input parameters for the Modify Order API,
8
+ # ensuring that all required fields are provided and optional fields follow
9
+ # the correct constraints. It also applies custom validation rules based on
10
+ # the type of order.
11
+ #
12
+ # Example usage:
13
+ # contract = Dhanhq::Contracts::ModifyOrderContract.new
14
+ # result = contract.call(
15
+ # dhanClientId: "123456",
16
+ # orderId: "1001",
17
+ # orderType: "STOP_LOSS",
18
+ # legName: "ENTRY_LEG",
19
+ # quantity: 10,
20
+ # price: 150.0,
21
+ # triggerPrice: 140.0,
22
+ # validity: "DAY"
23
+ # )
24
+ # result.success? # => true or false
25
+ #
26
+ # @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
27
+ class ModifyOrderContract < BaseContract
28
+ # Parameters and validation rules for the Modify Order request.
29
+ #
30
+ # @!attribute [r] orderId
31
+ # @return [String] Required. Unique identifier for the order to be modified.
32
+ # @!attribute [r] orderType
33
+ # @return [String] Required. Type of the order.
34
+ # Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
35
+ # @!attribute [r] legName
36
+ # @return [String] Optional. Leg name for complex orders.
37
+ # Must be one of: ENTRY_LEG, TARGET_LEG, STOP_LOSS_LEG, NA.
38
+ # @!attribute [r] quantity
39
+ # @return [Integer] Required. Quantity to be modified, must be greater than 0.
40
+ # @!attribute [r] price
41
+ # @return [Float] Optional. Price to be modified, must be greater than 0 if provided.
42
+ # @!attribute [r] disclosedQuantity
43
+ # @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
44
+ # @!attribute [r] triggerPrice
45
+ # @return [Float] Optional. Trigger price for stop-loss orders, must be greater than 0 if provided.
46
+ # @!attribute [r] validity
47
+ # @return [String] Required. Validity of the order.
48
+ # Must be one of: DAY, IOC, GTC, GTD.
49
+ params do
50
+ required(:orderId).filled(:string)
51
+ required(:orderType).filled(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
52
+ optional(:legName).maybe(:string, included_in?: %w[ENTRY_LEG TARGET_LEG STOP_LOSS_LEG NA])
53
+ required(:quantity).filled(:integer, gt?: 0)
54
+ optional(:price).maybe(:float, gt?: 0)
55
+ optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
56
+ optional(:triggerPrice).maybe(:float, gt?: 0)
57
+ required(:validity).filled(:string, included_in?: %w[DAY IOC GTC GTD])
58
+ end
59
+
60
+ # Custom validation to ensure a trigger price is provided for stop-loss orders.
61
+ #
62
+ # @example Invalid stop-loss order:
63
+ # orderType: "STOP_LOSS", triggerPrice: nil
64
+ # => Adds failure message "is required for orderType STOP_LOSS or STOP_LOSS_MARKET".
65
+ #
66
+ # @param triggerPrice [Float] The price at which the order will be triggered.
67
+ # @param orderType [String] The type of the order.
68
+ rule(:triggerPrice, :orderType) do
69
+ if values[:orderType].start_with?("STOP_LOSS") && !values[:triggerPrice]
70
+ key(:triggerPrice).failure("is required for orderType STOP_LOSS or STOP_LOSS_MARKET")
71
+ end
72
+ end
73
+
74
+ # Custom validation to ensure a leg name is provided for CO or BO order types.
75
+ #
76
+ # @example Invalid CO order:
77
+ # orderType: "CO", legName: nil
78
+ # => Adds failure message "is required for orderType CO or BO".
79
+ #
80
+ # @param legName [String] The leg name of the order.
81
+ # @param orderType [String] The type of the order.
82
+ rule(:legName, :orderType) do
83
+ if %w[CO BO].include?(values[:orderType]) && !values[:legName]
84
+ key(:legName).failure("is required for orderType CO or BO")
85
+ end
86
+ end
87
+
88
+ # Custom validation to ensure the price is valid if provided.
89
+ #
90
+ # @example Invalid price:
91
+ # price: 0
92
+ # => Adds failure message "must be greater than 0 if provided".
93
+ #
94
+ # @param price [Float] The price of the order.
95
+ rule(:price) do
96
+ key(:price).failure("must be greater than 0 if provided") if values[:price].nil? || values[:price] <= 0
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ class ModifyOrderContract < Dry::Validation::Contract
6
+ params do
7
+ required(:dhanClientId).filled(:string)
8
+ required(:orderId).filled(:string)
9
+ optional(:orderType).maybe(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
10
+ optional(:quantity).maybe(:integer)
11
+ optional(:price).maybe(:float)
12
+ optional(:triggerPrice).maybe(:float)
13
+ optional(:disclosedQuantity).maybe(:integer)
14
+ optional(:validity).maybe(:string, included_in?: %w[DAY IOC])
15
+ end
16
+
17
+ rule(:quantity) do
18
+ key.failure("must be provided if modifying quantity") if value.nil? && values[:price].nil?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_contract"
4
+
5
+ module DhanHQ
6
+ module Contracts
7
+ # **Validation contract for fetching option chain data**
8
+ #
9
+ # Validates request parameters for fetching option chains & expiry lists.
10
+ class OptionChainContract < BaseContract
11
+ params do
12
+ required(:underlying_scrip).filled(:integer) # Security ID
13
+ required(:underlying_seg).filled(:string, included_in?: %w[IDX_I NSE_FNO BSE_FNO MCX_FO])
14
+ required(:expiry).filled(:string)
15
+ end
16
+
17
+ rule(:expiry) do
18
+ # Ensure the expiry date is in "YYYY-MM-DD" format
19
+ key.failure("must be in 'YYYY-MM-DD' format") unless value.match?(/^\d{4}-\d{2}-\d{2}$/)
20
+
21
+ # Ensure it is a valid date
22
+ begin
23
+ parsed_date = Date.parse(value)
24
+ key.failure("must be a valid date") unless parsed_date.to_s == value
25
+ rescue ArgumentError
26
+ key.failure("is not a valid date")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/dhan_hq/contracts/order_contract.rb
4
+ require "dry-validation"
5
+
6
+ module DhanHQ
7
+ module Contracts
8
+ # Shared contract used to validate place and modify order payloads.
9
+ class OrderContract < BaseContract
10
+ # Allowed transaction directions supported by DhanHQ order APIs.
11
+ TRANSACTION_TYPES = %w[BUY SELL].freeze
12
+ # Supported exchange segments for order placement requests.
13
+ EXCHANGE_SEGMENTS = %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ MCX_COMM BSE_CURRENCY BSE_FNO].freeze
14
+ # Permitted product types for order placement.
15
+ PRODUCT_TYPES = %w[CNC INTRADAY MARGIN CO BO].freeze
16
+ # Supported order execution types.
17
+ ORDER_TYPES = %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET].freeze
18
+ # Validity window options for orders.
19
+ VALIDITY_TYPES = %w[DAY IOC].freeze
20
+ # After-market execution windows.
21
+ AMO_TIMES = %w[PRE_OPEN OPEN OPEN_30 OPEN_60].freeze
22
+
23
+ params do
24
+ # Common required fields
25
+ required(:dhan_client_id).filled(:string)
26
+ required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
27
+ required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
28
+ required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
29
+ required(:order_type).filled(:string, included_in?: ORDER_TYPES)
30
+ required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
31
+ required(:security_id).filled(:string)
32
+ required(:quantity).filled(:integer, gt?: 0)
33
+
34
+ # Optional fields
35
+ optional(:correlation_id).maybe(:string)
36
+ optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
37
+ optional(:price).maybe(:float)
38
+ optional(:trigger_price).maybe(:float)
39
+ optional(:after_market_order).maybe(:bool)
40
+ optional(:amo_time).maybe(:string, included_in?: AMO_TIMES)
41
+ optional(:bo_profit_value).maybe(:float)
42
+ optional(:bo_stop_loss_value).maybe(:float)
43
+ optional(:leg_name).maybe(:string) # For modifications
44
+ end
45
+
46
+ # Conditional validation rules
47
+ rule(:price) do
48
+ key.failure("must be present for LIMIT orders") if values[:order_type] == "LIMIT" && !value
49
+ end
50
+
51
+ rule(:trigger_price) do
52
+ if %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type]) && !value
53
+ key.failure("must be present for STOP_LOSS orders")
54
+ end
55
+ end
56
+
57
+ rule(:amo_time) do
58
+ key.failure("must be present for after market orders") if values[:after_market_order] == true && !value
59
+ end
60
+
61
+ rule(:bo_profit_value, :bo_stop_loss_value) do
62
+ if values[:product_type] == "BO" && (!values[:bo_profit_value] || !values[:bo_stop_loss_value])
63
+ key.failure("both profit and stop loss values required for BO orders")
64
+ end
65
+ end
66
+
67
+ rule(:disclosed_quantity) do
68
+ key.failure("cannot exceed 30% of total quantity") if value && value > (values[:quantity] * 0.3)
69
+ end
70
+
71
+ # Modification specific rules (when extending)
72
+ rule(:leg_name) do
73
+ if values[:product_type] == "BO" && !%w[ENTRY_LEG TARGET_LEG STOP_LOSS_LEG].include?(value)
74
+ key.failure("invalid leg name for BO order")
75
+ end
76
+ end
77
+ end
78
+
79
+ # Contract enforcing additional requirements for new order placement.
80
+ class PlaceOrderContract < OrderContract
81
+ # Additional placement specific rules
82
+ rule(:after_market_order) do
83
+ key.failure("amo_time required for after market orders") if value == true && !values[:amo_time]
84
+ end
85
+ end
86
+
87
+ # Contract extending {OrderContract} for order modification payloads.
88
+ class ModifyOrderContract < OrderContract
89
+ # Modification specific requirements
90
+ params do
91
+ required(:order_id).filled(:string)
92
+ optional(:quantity).maybe(:integer, gt?: 0)
93
+ end
94
+
95
+ rule do
96
+ if !values[:price] && !values[:quantity] && !values[:trigger_price]
97
+ key.failure("at least one modification field required")
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validation contract for placing an order via Dhanhq's API.
6
+ #
7
+ # This contract validates the parameters required to place an order,
8
+ # ensuring the correctness of inputs based on API requirements. It includes:
9
+ # - Mandatory fields for order placement.
10
+ # - Conditional validation for optional fields based on provided values.
11
+ # - Validation of enumerated values using constants for consistency.
12
+ #
13
+ # Example usage:
14
+ # contract = Dhanhq::Contracts::PlaceOrderContract.new
15
+ # result = contract.call(
16
+ # dhanClientId: "123456",
17
+ # transaction_type: "BUY",
18
+ # exchange_segment: "NSE_EQ",
19
+ # product_type: "CNC",
20
+ # order_type: "LIMIT",
21
+ # validity: "DAY",
22
+ # security_id: "1001",
23
+ # quantity: 10,
24
+ # price: 150.0
25
+ # )
26
+ # result.success? # => true or false
27
+ #
28
+ # @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
29
+ class PlaceOrderContract < BaseContract
30
+ # Parameters and validation rules for the place order request.
31
+ #
32
+ # @!attribute [r] correlation_id
33
+ # @return [String] Optional. Identifier for tracking, max length 25 characters.
34
+ # @!attribute [r] transaction_type
35
+ # @return [String] Required. BUY or SELL.
36
+ # @!attribute [r] exchange_segment
37
+ # @return [String] Required. Exchange segment for the order.
38
+ # Must be one of: `EXCHANGE_SEGMENTS`.
39
+ # @!attribute [r] product_type
40
+ # @return [String] Required. Product type for the order.
41
+ # Must be one of: `PRODUCT_TYPES`.
42
+ # @!attribute [r] order_type
43
+ # @return [String] Required. Type of order.
44
+ # Must be one of: `ORDER_TYPES`.
45
+ # @!attribute [r] validity
46
+ # @return [String] Required. Validity of the order.
47
+ # Must be one of: DAY, IOC.
48
+ # @!attribute [r] trading_symbol
49
+ # @return [String] Optional. Trading symbol of the instrument.
50
+ # @!attribute [r] security_id
51
+ # @return [String] Required. Security identifier for the order.
52
+ # @!attribute [r] quantity
53
+ # @return [Integer] Required. Quantity of the order, must be greater than 0.
54
+ # @!attribute [r] disclosed_quantity
55
+ # @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
56
+ # @!attribute [r] price
57
+ # @return [Float] Optional. Price for the order, must be > 0 if provided.
58
+ # @!attribute [r] trigger_price
59
+ # @return [Float] Optional. Trigger price for stop-loss orders, must be > 0 if provided.
60
+ # @!attribute [r] after_market_order
61
+ # @return [Boolean] Optional. Indicates if this is an after-market order.
62
+ # @!attribute [r] amo_time
63
+ # @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
64
+ # @!attribute [r] bo_profit_value
65
+ # @return [Float] Optional. Profit value for Bracket Orders, must be > 0 if provided.
66
+ # @!attribute [r] bo_stop_loss_value
67
+ # @return [Float] Optional. Stop-loss value for Bracket Orders, must be > 0 if provided.
68
+ # @!attribute [r] drv_expiry_date
69
+ # @return [String] Optional. Expiry date for derivative contracts.
70
+ # @!attribute [r] drv_option_type
71
+ # @return [String] Optional. Option type for derivatives, must be one of: CALL, PUT, NA.
72
+ # @!attribute [r] drv_strike_price
73
+ # @return [Float] Optional. Strike price for options, must be > 0 if provided.
74
+ params do
75
+ required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
76
+ required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
77
+ required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
78
+ required(:order_type).filled(:string, included_in?: ORDER_TYPES)
79
+ required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
80
+ required(:security_id).filled(:string)
81
+ required(:quantity).filled(:integer, gt?: 0)
82
+ optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
83
+ optional(:trading_symbol).maybe(:string)
84
+ optional(:correlation_id).maybe(:string, max_size?: 25)
85
+ optional(:price).maybe(:float, gt?: 0)
86
+ optional(:trigger_price).maybe(:float, gt?: 0)
87
+ optional(:after_market_order).maybe(:bool)
88
+ optional(:amo_time).maybe(:string, included_in?: %w[OPEN OPEN_30 OPEN_60])
89
+ optional(:bo_profit_value).maybe(:float, gt?: 0)
90
+ optional(:bo_stop_loss_value).maybe(:float, gt?: 0)
91
+ optional(:drv_expiry_date).maybe(:string)
92
+ optional(:drv_option_type).maybe(:string, included_in?: %w[CALL PUT NA])
93
+ optional(:drv_strike_price).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(:trigger_price, :order_type) do
98
+ if values[:order_type] =~ /^STOP_LOSS/ && !values[:trigger_price]
99
+ key(:trigger_price).failure("is required for order_type 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(:after_market_order, :amo_time) do
105
+ if values[:after_market_order] == true && !values[:amo_time]
106
+ key(:amo_time).failure("is required when after_market_order is true")
107
+ end
108
+ end
109
+
110
+ # Custom validation for Bracket Order (BO) fields.
111
+ rule(:bo_profit_value, :bo_stop_loss_value, :product_type) do
112
+ if values[:product_type] == "BO" && (!values[:bo_profit_value] || !values[:bo_stop_loss_value])
113
+ key(:bo_profit_value).failure("is required for Bracket Orders")
114
+ key(:bo_stop_loss_value).failure("is required for Bracket Orders")
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validates requests for converting positions between product types.
6
+ class PositionConversionContract < BaseContract
7
+ params do
8
+ required(:dhanClientId).filled(:string)
9
+ required(:fromProductType).filled(:string, included_in?: PRODUCT_TYPES)
10
+ required(:exchangeSegment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
11
+ required(:positionType).filled(:string, included_in?: %w[LONG SHORT CLOSED])
12
+ required(:securityId).filled(:string)
13
+ required(:convertQty).filled(:integer, gt?: 0)
14
+ required(:toProductType).filled(:string, included_in?: PRODUCT_TYPES)
15
+ end
16
+
17
+ rule(:toProductType, :fromProductType) do
18
+ next unless values[:toProductType] == values[:fromProductType]
19
+
20
+ key(:toProductType).failure("must be different from fromProductType")
21
+ end
22
+ end
23
+ end
24
+ end