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,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
|