DhanHQ 4.0.0 → 4.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 +4 -4
- data/CHANGELOG.md +39 -0
- data/GUIDE.md +24 -0
- data/README.md +75 -0
- data/docs/CONFIGURATION.md +4 -2
- data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
- data/lib/DhanHQ/client.rb +30 -5
- data/lib/DhanHQ/concerns/order_audit.rb +26 -3
- data/lib/DhanHQ/configuration.rb +152 -3
- data/lib/DhanHQ/constants.rb +60 -1
- data/lib/DhanHQ/contracts/company_info_contract.rb +24 -0
- data/lib/DhanHQ/contracts/market_movers_contract.rb +57 -0
- data/lib/DhanHQ/contracts/news_headline_contract.rb +21 -0
- data/lib/DhanHQ/contracts/technical_data_contract.rb +25 -0
- data/lib/DhanHQ/core/base_api.rb +0 -15
- data/lib/DhanHQ/core/base_model.rb +32 -12
- data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
- data/lib/DhanHQ/models/option_chain.rb +7 -0
- data/lib/DhanHQ/models/order.rb +7 -7
- data/lib/DhanHQ/rate_limiter.rb +34 -40
- data/lib/DhanHQ/resources/global_stocks/funds.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/orders.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/trades.rb +1 -1
- data/lib/DhanHQ/resources/scanx.rb +98 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +5 -10
- data/lib/DhanHQ/ws/connection.rb +13 -15
- data/lib/DhanHQ/ws/market_depth/client.rb +5 -1
- data/lib/DhanHQ/ws/registry.rb +10 -6
- data/lib/dhan_hq.rb +7 -2
- metadata +6 -1
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -15,6 +15,24 @@ module DhanHQ
|
|
|
15
15
|
# Default Sandbox API host.
|
|
16
16
|
# @return [String]
|
|
17
17
|
SANDBOX_URL = Constants::Urls::SANDBOX_API_BASE
|
|
18
|
+
|
|
19
|
+
# All environments a client can run in.
|
|
20
|
+
#
|
|
21
|
+
# - +:live+ — every request hits the production host.
|
|
22
|
+
# - +:sandbox+ — every request hits the sandbox host (previous `sandbox = true` behaviour).
|
|
23
|
+
# - +:hybrid+ — market data (quotes, option chain, charts, instruments, ScanX)
|
|
24
|
+
# hits production while trading/account requests hit the sandbox host, so a
|
|
25
|
+
# strategy rehearses execution against real prices without risking money.
|
|
26
|
+
# @return [Array<Symbol>]
|
|
27
|
+
MODES = %i[live sandbox hybrid].freeze
|
|
28
|
+
|
|
29
|
+
# API tiers routed to the production host when {#mode} is +:hybrid+.
|
|
30
|
+
# Everything else (orders, positions, funds, statements, ...) routes to the
|
|
31
|
+
# sandbox host. Global Stocks keeps its own tier so it stays on production in
|
|
32
|
+
# hybrid mode — the sandbox host does not mirror the US-equities API.
|
|
33
|
+
# @return [Array<Symbol>]
|
|
34
|
+
HYBRID_LIVE_API_TYPES = %i[data_api quote_api option_chain global_stocks_api].freeze
|
|
35
|
+
|
|
18
36
|
# The client ID for API authentication.
|
|
19
37
|
# @return [String, nil] The client ID or `nil` if not set.
|
|
20
38
|
attr_accessor :client_id
|
|
@@ -39,7 +57,55 @@ module DhanHQ
|
|
|
39
57
|
attr_writer :base_url
|
|
40
58
|
|
|
41
59
|
# Whether to use the sandbox environment.
|
|
42
|
-
|
|
60
|
+
#
|
|
61
|
+
# Backward-compatible facade over {#mode}: setting it to +true+ switches to
|
|
62
|
+
# full sandbox mode, +false+ back to live. Prefer {#mode} for new code — it
|
|
63
|
+
# also supports +:hybrid+ (live market data + sandbox execution).
|
|
64
|
+
#
|
|
65
|
+
# rubocop:disable Naming/PredicateMethod -- legacy attr_accessor-style API,
|
|
66
|
+
# kept as a boolean mirror of +mode+ for every caller written before :hybrid.
|
|
67
|
+
# @return [Boolean] True when every request targets the sandbox host.
|
|
68
|
+
def sandbox
|
|
69
|
+
mode == :sandbox
|
|
70
|
+
end
|
|
71
|
+
# rubocop:enable Naming/PredicateMethod
|
|
72
|
+
|
|
73
|
+
def sandbox=(value)
|
|
74
|
+
self.mode = value ? :sandbox : :live
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The environment the client runs in: +:live+, +:sandbox+, or +:hybrid+.
|
|
78
|
+
#
|
|
79
|
+
# Set via +DHAN_MODE+ or in {DhanHQ.configure}. {#sandbox=} remains as an
|
|
80
|
+
# alias for switching between +:live+ and +:sandbox+.
|
|
81
|
+
#
|
|
82
|
+
# @return [Symbol]
|
|
83
|
+
attr_reader :mode
|
|
84
|
+
|
|
85
|
+
# @param value [Symbol, String] One of {MODES} (case-insensitive).
|
|
86
|
+
# @raise [ArgumentError] When the value is not a known mode.
|
|
87
|
+
def mode=(value)
|
|
88
|
+
@mode = coerce_mode!(value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Per-tier environment overrides applied on top of {#mode}.
|
|
92
|
+
#
|
|
93
|
+
# Keys are API tier symbols (+:order_api+, +:data_api+, +:quote_api+,
|
|
94
|
+
# +:option_chain+, +:non_trading_api+, +:global_stocks_api+); values are
|
|
95
|
+
# +:live+ or +:sandbox+. Useful to, say, keep fund limits on production while
|
|
96
|
+
# everything else runs on the sandbox in hybrid mode:
|
|
97
|
+
#
|
|
98
|
+
# DhanHQ.configure do |c|
|
|
99
|
+
# c.mode = :hybrid
|
|
100
|
+
# c.environment_overrides = { non_trading_api: :live }
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @return [Hash{Symbol => Symbol}]
|
|
104
|
+
attr_reader :environment_overrides
|
|
105
|
+
|
|
106
|
+
def environment_overrides=(value)
|
|
107
|
+
@environment_overrides = coerce_environment_overrides(value)
|
|
108
|
+
end
|
|
43
109
|
|
|
44
110
|
# When true, state-changing requests (order placement, modification,
|
|
45
111
|
# cancellation, position exits, trading controls) are validated and logged but
|
|
@@ -176,15 +242,54 @@ module DhanHQ
|
|
|
176
242
|
# is nil or the default production URL, returns {SANDBOX_URL}.
|
|
177
243
|
# @return [String]
|
|
178
244
|
def base_url
|
|
179
|
-
|
|
245
|
+
base_url_for(nil)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Returns the base URL for requests on the given API tier, honouring
|
|
249
|
+
# {#mode}, {#environment_overrides}, and an explicit {#base_url} override.
|
|
250
|
+
#
|
|
251
|
+
# In +:hybrid+ mode data tiers (see {HYBRID_LIVE_API_TYPES}) resolve to the
|
|
252
|
+
# production host while trading and account tiers resolve to the sandbox
|
|
253
|
+
# host, which is what lets a strategy read live prices while placing paper
|
|
254
|
+
# orders.
|
|
255
|
+
#
|
|
256
|
+
# @param api_type [Symbol, nil] API tier the request belongs to. +nil+ uses
|
|
257
|
+
# the mode-level default (+:sandbox+ mode resolves to the sandbox host,
|
|
258
|
+
# +:live+ and +:hybrid+ to production).
|
|
259
|
+
# @return [String]
|
|
260
|
+
def base_url_for(api_type)
|
|
261
|
+
if environment_for(api_type) == :sandbox && (@base_url.nil? || @base_url == BASE_URL)
|
|
180
262
|
SANDBOX_URL
|
|
181
263
|
else
|
|
182
264
|
@base_url || BASE_URL
|
|
183
265
|
end
|
|
184
266
|
end
|
|
185
267
|
|
|
268
|
+
# The effective environment (+:live+ or +:sandbox+) for requests on the
|
|
269
|
+
# given API tier. Explicit {#environment_overrides} win over the {#mode}
|
|
270
|
+
# defaults.
|
|
271
|
+
#
|
|
272
|
+
# @param api_type [Symbol, nil]
|
|
273
|
+
# @return [Symbol]
|
|
274
|
+
def environment_for(api_type)
|
|
275
|
+
key = api_type&.to_sym
|
|
276
|
+
return @environment_overrides[key] if key && @environment_overrides.key?(key)
|
|
277
|
+
|
|
278
|
+
case mode
|
|
279
|
+
when :sandbox then :sandbox
|
|
280
|
+
when :hybrid then HYBRID_LIVE_API_TYPES.include?(key) ? :live : :sandbox
|
|
281
|
+
else :live
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
186
285
|
def sandbox?
|
|
187
|
-
|
|
286
|
+
mode == :sandbox
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# True when {#mode} is +:hybrid+ — live market data, sandbox execution.
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def hybrid?
|
|
292
|
+
mode == :hybrid
|
|
188
293
|
end
|
|
189
294
|
|
|
190
295
|
# @return [Boolean] True when state-changing requests should be simulated.
|
|
@@ -228,6 +333,10 @@ module DhanHQ
|
|
|
228
333
|
default: true)
|
|
229
334
|
end
|
|
230
335
|
@base_url = attrs[:base_url] || ENV.fetch("DHAN_BASE_URL", nil)
|
|
336
|
+
@environment_overrides = coerce_environment_overrides(
|
|
337
|
+
attrs[:environment_overrides] || ENV.fetch("DHAN_ENV_OVERRIDES", nil)
|
|
338
|
+
)
|
|
339
|
+
@mode = resolve_mode(attrs)
|
|
231
340
|
@ws_version = (attrs[:ws_version] || ENV.fetch("DHAN_WS_VERSION", 2)).to_i
|
|
232
341
|
@ws_order_url = attrs[:ws_order_url] || ENV.fetch("DHAN_WS_ORDER_URL", nil)
|
|
233
342
|
@ws_market_feed_url = attrs[:ws_market_feed_url] || ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
|
|
@@ -242,6 +351,46 @@ module DhanHQ
|
|
|
242
351
|
|
|
243
352
|
private
|
|
244
353
|
|
|
354
|
+
# Resolves the initial mode from attributes and ENV.
|
|
355
|
+
#
|
|
356
|
+
# Precedence: an explicit +:mode+ attribute wins, then +DHAN_MODE+, then the
|
|
357
|
+
# legacy +sandbox+ attribute / +DHAN_SANDBOX+ flag, then +:live+.
|
|
358
|
+
def resolve_mode(attrs)
|
|
359
|
+
raw_mode = attrs.key?(:mode) ? attrs[:mode] : ENV.fetch("DHAN_MODE", nil)
|
|
360
|
+
return coerce_mode!(raw_mode) if raw_mode
|
|
361
|
+
|
|
362
|
+
sandbox_flag = attrs.key?(:sandbox) ? attrs[:sandbox] : env_flag("DHAN_SANDBOX", default: false)
|
|
363
|
+
sandbox_flag ? :sandbox : :live
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Normalises a mode value, raising on anything outside {MODES}.
|
|
367
|
+
def coerce_mode!(value)
|
|
368
|
+
mode = value.to_s.strip.downcase.to_sym
|
|
369
|
+
raise ArgumentError, "unknown mode #{value.inspect}: expected one of #{MODES.join(", ")}" unless MODES.include?(mode)
|
|
370
|
+
|
|
371
|
+
mode
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Valid targets for an environment override.
|
|
375
|
+
OVERRIDE_TARGETS = %i[live sandbox].freeze
|
|
376
|
+
private_constant :OVERRIDE_TARGETS
|
|
377
|
+
|
|
378
|
+
# Normalises an environment-overrides hash: symbol keys, :live/:sandbox values.
|
|
379
|
+
def coerce_environment_overrides(value)
|
|
380
|
+
return {} if value.nil? || value == ""
|
|
381
|
+
|
|
382
|
+
value = value.split(",").to_h { |pair| pair.split("=") } if value.is_a?(String)
|
|
383
|
+
raise ArgumentError, "environment_overrides must be a Hash (tier => :live/:sandbox)" unless value.is_a?(Hash)
|
|
384
|
+
|
|
385
|
+
value.each_with_object({}) do |(tier, env), out|
|
|
386
|
+
key = tier.to_s.strip.to_sym
|
|
387
|
+
target = env.to_s.strip.downcase.to_sym
|
|
388
|
+
raise ArgumentError, "environment override for #{key} must be :live or :sandbox (got #{env.inspect})" unless OVERRIDE_TARGETS.include?(target)
|
|
389
|
+
|
|
390
|
+
out[key] = target
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
245
394
|
# Reads a boolean-ish environment variable, falling back to +default+ when unset.
|
|
246
395
|
def env_flag(name, default:)
|
|
247
396
|
raw = ENV.fetch(name, nil)
|
data/lib/DhanHQ/constants.rb
CHANGED
|
@@ -130,6 +130,63 @@ module DhanHQ
|
|
|
130
130
|
# Backward-compatible alias kept for existing SDK usage.
|
|
131
131
|
Instrument = InstrumentType
|
|
132
132
|
|
|
133
|
+
# ScanX data API enums (POST /v2/data/* — company info, market movers,
|
|
134
|
+
# news headlines, technical indicators).
|
|
135
|
+
module ScanX
|
|
136
|
+
# Exchange segments accepted by POST /v2/data/companyinfo.
|
|
137
|
+
COMPANY_INFO_SEGMENTS = %w[NSE_EQ BSE_EQ].freeze
|
|
138
|
+
# Metric sections returned by POST /v2/data/companyinfo (CO = company
|
|
139
|
+
# overview, RATIOS = valuation/profitability + industry benchmarks,
|
|
140
|
+
# SHP = shareholding pattern).
|
|
141
|
+
COMPANY_INFO_METRICS = %w[CO RATIOS SHP].freeze
|
|
142
|
+
# Instrument kinds accepted by POST /v2/data/companyinfo.
|
|
143
|
+
COMPANY_INFO_INSTRUMENTS = %w[EQUITY].freeze
|
|
144
|
+
|
|
145
|
+
# Exchange segments accepted by POST /v2/data/marketmovers.
|
|
146
|
+
MARKET_MOVERS_SEGMENTS = %w[NSE_FNO BSE_FNO NSE_COMM MCX_COMM NSE_EQ BSE_EQ].freeze
|
|
147
|
+
# Instrument kinds ranked by POST /v2/data/marketmovers.
|
|
148
|
+
MARKET_MOVERS_INSTRUMENTS = %w[OPTIDX OPTSTK OPTFUT FUTIDX FUTSTK FUTCOM EQUITY].freeze
|
|
149
|
+
# Ranking categories for POST /v2/data/marketmovers.
|
|
150
|
+
MARKET_MOVERS_CATEGORIES =
|
|
151
|
+
%w[HIGHEST_OI OI_GAINERS OI_LOSERS TOP_VOLUME PRICE_GAINERS PRICE_LOSERS].freeze
|
|
152
|
+
# Equity universes accepted by POST /v2/data/marketmovers (required when
|
|
153
|
+
# instrument is EQUITY).
|
|
154
|
+
MARKET_MOVERS_UNIVERSES = %w[
|
|
155
|
+
ALL FNO_STOCKS NIFTY_50 NIFTY_BANK FINNIFTY INDIA_VIX NIFTY_MIDCAP NIFTY_NEXT_50
|
|
156
|
+
NIFTY_SMALLCAP_50 NIFTY_MID_CAP_50 NIFTY_100 NIFTY_200 NIFTY_500 NIFTY_MIDCAP_100
|
|
157
|
+
NIFTY_MIDCAP_150 NIFTY_SMALLCAP_100 NIFTY_SMALLCAP_250 NIFTY_MICROCAP_250 NIFTY_AUTO
|
|
158
|
+
NIFTY_PRIVATE_BANK NIFTY_FMCG NIFTY_ENERGY NIFTY_INFRA NIFTY_IT NIFTY_MEDIA NIFTY_METAL
|
|
159
|
+
NIFTY_MNC NIFTY_PHARMA NIFTY_PSU_BANK NIFTY_REALTY NIFTY_SERVICE_SECTOR NIFTY_CUNSUMPTION
|
|
160
|
+
GIFT_NIFTY SENSEX BSE_100 BSE_200 BSE_500 BSE_150_MIDCAP BSE_250_SMALLCAP
|
|
161
|
+
BSE_250_LARGE_MID BSE_400_MID_SMALL BSE_BANKEX BSE_AUTO BSE_CAPITAL_GOODS
|
|
162
|
+
BSE_CONSUMER_DURABLES BSE_ENERGY BSE_FINANCE BSE_FMCG BSE_HEALTHCARE BSE_INDIA_MFG
|
|
163
|
+
BSE_INDUSTRIALS BSE_IPO BSE_IT BSE_METALS BSE_OIL_AND_GAS BSE_POWER BSE_PSU BSE_TELECOM
|
|
164
|
+
].freeze
|
|
165
|
+
|
|
166
|
+
# News categories accepted by POST /v2/data/newsheadline.
|
|
167
|
+
NEWS_CATEGORIES = %w[
|
|
168
|
+
ALL COMPANIES EQUITY_MARKETS DEBT_MARKETS IPO GLOBAL INDIAN_ECONOMY GLOBAL_ECONOMY
|
|
169
|
+
CURRENCY COMMODITIES CRYPTOCURRENCIES GOVERNMENT_POLICY_AND_REGULATION INFRASTRUCTURE
|
|
170
|
+
INTERNATIONAL_TRADE INVESTMENT_IDEAS REAL_ESTATE STARTUPS TECHNOLOGY MUTUAL_FUNDS
|
|
171
|
+
].freeze
|
|
172
|
+
# News scoping options (portfolio/watchlist) for POST /v2/data/newsheadline.
|
|
173
|
+
NEWS_UNIVERSES = %w[PORTFOLIO WATCHLIST].freeze
|
|
174
|
+
|
|
175
|
+
# Exchange segments accepted by POST /v2/data/technical.
|
|
176
|
+
TECHNICAL_SEGMENTS = %w[NSE_EQ IDX_I].freeze
|
|
177
|
+
# Instrument kinds accepted by POST /v2/data/technical.
|
|
178
|
+
TECHNICAL_INSTRUMENTS = %w[INDEX EQUITY].freeze
|
|
179
|
+
# Candle timeframes for POST /v2/data/technical (1/5/15 minute, D = daily).
|
|
180
|
+
TECHNICAL_TIMEFRAMES = %w[1 5 15 D].freeze
|
|
181
|
+
# Indicator identifiers computed by POST /v2/data/technical.
|
|
182
|
+
TECHNICAL_INDICATORS = %w[
|
|
183
|
+
SMA_5 SMA_10 SMA_20 SMA_50 SMA_100 SMA_200
|
|
184
|
+
EMA_5 EMA_10 EMA_20 EMA_50 EMA_100 EMA_200
|
|
185
|
+
RSI_14 MACD_HIST STOCH STOCHRSI_14 ATR_14 ADX_14 UO ROC WILLR
|
|
186
|
+
PIVOT_CLASSIC PIVOT_FIBONACCI PIVOT_CAMARILLA
|
|
187
|
+
].freeze
|
|
188
|
+
end
|
|
189
|
+
|
|
133
190
|
# Minute intervals allowed by POST /v2/charts/intraday (charts annexure).
|
|
134
191
|
module ChartInterval
|
|
135
192
|
ONE = "1"
|
|
@@ -536,7 +593,8 @@ module DhanHQ
|
|
|
536
593
|
"/v2/marketfeed/",
|
|
537
594
|
"/v2/optionchain",
|
|
538
595
|
"/v2/instrument/",
|
|
539
|
-
"/v2/charts"
|
|
596
|
+
"/v2/charts",
|
|
597
|
+
"/v2/data"
|
|
540
598
|
].freeze
|
|
541
599
|
|
|
542
600
|
# Path prefixes for which the request body (POST/PUT/PATCH) must include dhanClientId.
|
|
@@ -555,6 +613,7 @@ module DhanHQ
|
|
|
555
613
|
/v2/killswitch
|
|
556
614
|
/v2/ip
|
|
557
615
|
/v2/globalstocks
|
|
616
|
+
/v2/data/newsheadline
|
|
558
617
|
].freeze
|
|
559
618
|
|
|
560
619
|
# Path prefixes whose non-GET requests change real account state — they place,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/data/companyinfo (ScanX company fundamentals).
|
|
6
|
+
#
|
|
7
|
+
# securityId (digits), exchangeSegment (NSE_EQ/BSE_EQ), instrument (EQUITY),
|
|
8
|
+
# metrics (one or more of CO / RATIOS / SHP).
|
|
9
|
+
class CompanyInfoContract < BaseContract
|
|
10
|
+
params do
|
|
11
|
+
required(:security_id).filled(:string)
|
|
12
|
+
required(:exchange_segment).filled(:string, included_in?: Constants::ScanX::COMPANY_INFO_SEGMENTS)
|
|
13
|
+
required(:instrument).filled(:string, included_in?: Constants::ScanX::COMPANY_INFO_INSTRUMENTS)
|
|
14
|
+
required(:metrics).filled(:array, min_size?: 1) do
|
|
15
|
+
each(:str?, included_in?: Constants::ScanX::COMPANY_INFO_METRICS)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
rule(:security_id) do
|
|
20
|
+
key.failure("must be a numeric security id string") unless value.to_s.match?(/\A\d+\z/)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Contracts
|
|
7
|
+
# Validates request for POST /v2/data/marketmovers (ScanX ranked instruments).
|
|
8
|
+
#
|
|
9
|
+
# exchangeSegment, instrument[] (options / futures / equity kinds, no mixing
|
|
10
|
+
# groups), category, limit (1-100). Expiry is required for options/futures;
|
|
11
|
+
# universe is required for EQUITY and ignored otherwise.
|
|
12
|
+
class MarketMoversContract < BaseContract
|
|
13
|
+
DERIVATIVE_INSTRUMENTS = %w[OPTIDX OPTSTK OPTFUT FUTIDX FUTSTK FUTCOM].freeze
|
|
14
|
+
private_constant :DERIVATIVE_INSTRUMENTS
|
|
15
|
+
|
|
16
|
+
params do
|
|
17
|
+
required(:exchange_segment).filled(:string, included_in?: Constants::ScanX::MARKET_MOVERS_SEGMENTS)
|
|
18
|
+
required(:instrument).filled(:array, min_size?: 1) do
|
|
19
|
+
each(:str?, included_in?: Constants::ScanX::MARKET_MOVERS_INSTRUMENTS)
|
|
20
|
+
end
|
|
21
|
+
required(:category).filled(:string, included_in?: Constants::ScanX::MARKET_MOVERS_CATEGORIES)
|
|
22
|
+
required(:limit).filled(:integer, included_in?: 1..100)
|
|
23
|
+
optional(:expiry).filled(:string)
|
|
24
|
+
optional(:universe).filled(:string, included_in?: Constants::ScanX::MARKET_MOVERS_UNIVERSES)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule(:expiry) do
|
|
28
|
+
next unless value.is_a?(String)
|
|
29
|
+
|
|
30
|
+
unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
31
|
+
key.failure("must be in YYYY-MM-DD format")
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Date.parse(value)
|
|
36
|
+
rescue StandardError
|
|
37
|
+
key.failure("must be a valid date")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Expiry is required when ranking derivatives; universe is required for EQUITY.
|
|
41
|
+
rule(:instrument, :expiry) do
|
|
42
|
+
instruments = Array(values[:instrument]).map(&:to_s)
|
|
43
|
+
key(:expiry).failure("is required when instrument includes options or futures") if derivative?(instruments) && values[:expiry].to_s.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
rule(:instrument, :universe) do
|
|
47
|
+
instruments = Array(values[:instrument]).map(&:to_s)
|
|
48
|
+
key(:universe).failure("is required when instrument is EQUITY") if instruments.include?(Constants::InstrumentType::EQUITY) && values[:universe].to_s.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# True when any requested instrument is an option or future.
|
|
52
|
+
def derivative?(instruments)
|
|
53
|
+
instruments.any? { |i| DERIVATIVE_INSTRUMENTS.include?(i) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/data/newsheadline (ScanX live news).
|
|
6
|
+
#
|
|
7
|
+
# categories[] (ALL or any news category), limit (1-50), optional universe
|
|
8
|
+
# (PORTFOLIO / WATCHLIST) and stock_list[] of security ids. dhanClientId is
|
|
9
|
+
# injected by the client layer and is not validated here.
|
|
10
|
+
class NewsHeadlineContract < BaseContract
|
|
11
|
+
params do
|
|
12
|
+
required(:categories).filled(:array, min_size?: 1) do
|
|
13
|
+
each(:str?, included_in?: Constants::ScanX::NEWS_CATEGORIES)
|
|
14
|
+
end
|
|
15
|
+
required(:limit).filled(:integer, included_in?: 1..50)
|
|
16
|
+
optional(:universe).filled(:string, included_in?: Constants::ScanX::NEWS_UNIVERSES)
|
|
17
|
+
optional(:stock_list).filled(:array) { each(:str?) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/data/technical (ScanX technical indicators).
|
|
6
|
+
#
|
|
7
|
+
# securityId (digits), exchangeSegment (NSE_EQ/IDX_I), instrument
|
|
8
|
+
# (INDEX/EQUITY), timeframe (1/5/15/D), indicators[] (e.g. SMA_20, RSI_14).
|
|
9
|
+
class TechnicalDataContract < BaseContract
|
|
10
|
+
params do
|
|
11
|
+
required(:security_id).filled(:string)
|
|
12
|
+
required(:exchange_segment).filled(:string, included_in?: Constants::ScanX::TECHNICAL_SEGMENTS)
|
|
13
|
+
required(:instrument).filled(:string, included_in?: Constants::ScanX::TECHNICAL_INSTRUMENTS)
|
|
14
|
+
required(:timeframe).filled(:string, included_in?: Constants::ScanX::TECHNICAL_TIMEFRAMES)
|
|
15
|
+
required(:indicators).filled(:array, min_size?: 1) do
|
|
16
|
+
each(:str?, included_in?: Constants::ScanX::TECHNICAL_INDICATORS)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
rule(:security_id) do
|
|
21
|
+
key.failure("must be a numeric security id string") unless value.to_s.match?(/\A\d+\z/)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/DhanHQ/core/base_api.rb
CHANGED
|
@@ -64,21 +64,6 @@ module DhanHQ
|
|
|
64
64
|
|
|
65
65
|
private
|
|
66
66
|
|
|
67
|
-
# Performs an API request.
|
|
68
|
-
#
|
|
69
|
-
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
70
|
-
# @param endpoint [String] API endpoint
|
|
71
|
-
# @param params [Hash] Request parameters
|
|
72
|
-
# @return [Hash, Array] The parsed API response
|
|
73
|
-
# @raise [DhanHQ::Error] If an API error occurs.
|
|
74
|
-
def request(method, endpoint, params: {})
|
|
75
|
-
formatted_params = format_params(endpoint, params)
|
|
76
|
-
|
|
77
|
-
response = client.request(method, build_path(endpoint), formatted_params)
|
|
78
|
-
|
|
79
|
-
handle_response(response)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
67
|
# Construct the complete API URL
|
|
83
68
|
#
|
|
84
69
|
# @param endpoint [String] API endpoint
|
|
@@ -40,14 +40,31 @@ module DhanHQ
|
|
|
40
40
|
class << self
|
|
41
41
|
attr_reader :defined_attributes
|
|
42
42
|
|
|
43
|
-
# Registers the set of attributes for this model
|
|
43
|
+
# Registers the set of attributes for this model. Also defines the
|
|
44
|
+
# snake_case and camelCase readers once per class, so instances stop
|
|
45
|
+
# allocating per-record singleton methods.
|
|
44
46
|
#
|
|
45
47
|
# @param args [Array<Symbol, String>] A list of attribute names
|
|
46
48
|
def attributes(*args)
|
|
47
49
|
@defined_attributes ||= []
|
|
48
|
-
|
|
50
|
+
new_attrs = args.map(&:to_s)
|
|
51
|
+
@defined_attributes.concat(new_attrs)
|
|
52
|
+
define_accessors(new_attrs)
|
|
53
|
+
@defined_attributes
|
|
49
54
|
end
|
|
50
55
|
|
|
56
|
+
# Defines one snake_case and one camelCase reader per attribute, reading
|
|
57
|
+
# through @attributes so every instance stays in sync. Called from
|
|
58
|
+
# {attributes}; private because it is a declaration helper.
|
|
59
|
+
def define_accessors(attrs)
|
|
60
|
+
attrs.each do |attr|
|
|
61
|
+
camel = attr.to_s.camelize(:lower)
|
|
62
|
+
define_method(attr) { @attributes[attr] }
|
|
63
|
+
define_method(camel) { @attributes[attr] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
private :define_accessors
|
|
67
|
+
|
|
51
68
|
# Provide a default API type, can be overridden by child classes
|
|
52
69
|
#
|
|
53
70
|
# e.g., def self.api_type; :data_api; end
|
|
@@ -64,6 +81,13 @@ module DhanHQ
|
|
|
64
81
|
@api ||= BaseAPI.new(api_type: api_type)
|
|
65
82
|
end
|
|
66
83
|
|
|
84
|
+
# Parameter key style used by {#to_request_params}: :camel (camelCase)
|
|
85
|
+
# by default. Models whose API takes TitleCase keys (the option chain)
|
|
86
|
+
# override this to :title.
|
|
87
|
+
def param_key_style
|
|
88
|
+
:camel
|
|
89
|
+
end
|
|
90
|
+
|
|
67
91
|
##
|
|
68
92
|
# Returns the API resource used by collection methods.
|
|
69
93
|
#
|
|
@@ -91,6 +115,8 @@ module DhanHQ
|
|
|
91
115
|
# Validate attributes before creating a new instance
|
|
92
116
|
def validate_attributes(attributes)
|
|
93
117
|
contract = validation_contract
|
|
118
|
+
return true if contract.nil?
|
|
119
|
+
|
|
94
120
|
result = contract.call(attributes)
|
|
95
121
|
|
|
96
122
|
raise ArgumentError, "Validation failed: #{result.errors.to_h}" if result.failure?
|
|
@@ -132,8 +158,6 @@ module DhanHQ
|
|
|
132
158
|
# @param attributes [Hash] The attributes of the resource
|
|
133
159
|
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
|
|
134
160
|
def create(attributes)
|
|
135
|
-
# validate_params!(attributes, validation_contract)
|
|
136
|
-
|
|
137
161
|
response = resource.post("", params: attributes)
|
|
138
162
|
build_from_response(response)
|
|
139
163
|
end
|
|
@@ -242,7 +266,7 @@ module DhanHQ
|
|
|
242
266
|
if self.class.defined_attributes&.include?("dhan_client_id") && !attrs[:dhan_client_id] && DhanHQ.configuration.client_id
|
|
243
267
|
attrs[:dhan_client_id] = DhanHQ.configuration.client_id
|
|
244
268
|
end
|
|
245
|
-
|
|
269
|
+
self.class.param_key_style == :title ? titleize_keys(attrs) : camelize_keys(attrs)
|
|
246
270
|
end
|
|
247
271
|
|
|
248
272
|
# Identifier inferred from the loaded attributes.
|
|
@@ -254,19 +278,15 @@ module DhanHQ
|
|
|
254
278
|
id_value&.to_s
|
|
255
279
|
end
|
|
256
280
|
|
|
257
|
-
#
|
|
281
|
+
# Assigns each declared attribute to an instance variable. The accessor
|
|
282
|
+
# methods themselves are defined once per class (see .attributes); the
|
|
283
|
+
# ivars exist for {AttributeHelper#inspect} and debugging.
|
|
258
284
|
def assign_attributes
|
|
259
285
|
self.class.defined_attributes&.each do |attr|
|
|
260
286
|
instance_variable_set(:"@#{attr}", @attributes[attr])
|
|
261
|
-
define_singleton_method(attr) { instance_variable_get(:"@#{attr}") }
|
|
262
|
-
define_singleton_method(attr.to_s.camelize(:lower)) { instance_variable_get(:"@#{attr}") }
|
|
263
287
|
end
|
|
264
288
|
end
|
|
265
289
|
|
|
266
|
-
def optionchain_api?
|
|
267
|
-
self.class.name.include?("OptionChain")
|
|
268
|
-
end
|
|
269
|
-
|
|
270
290
|
# Validate attributes using contract
|
|
271
291
|
def valid?
|
|
272
292
|
contract_class = validation_contract || self.class.validation_contract
|
|
@@ -69,27 +69,5 @@ module DhanHQ
|
|
|
69
69
|
instance_vars = self.class.defined_attributes.map { |attr| "#{attr}: #{instance_variable_get(:"@#{attr}")}" }
|
|
70
70
|
"#<#{self.class.name} #{instance_vars.join(", ")}>"
|
|
71
71
|
end
|
|
72
|
-
|
|
73
|
-
# def format_params(path, params)
|
|
74
|
-
# return params unless params.is_a?(Hash)
|
|
75
|
-
|
|
76
|
-
# if optionchain_api?(path)
|
|
77
|
-
# titleize_keys(params)
|
|
78
|
-
# else
|
|
79
|
-
# camelize_keys(params)
|
|
80
|
-
# end
|
|
81
|
-
# end
|
|
82
|
-
|
|
83
|
-
# def camelize_keys(hash)
|
|
84
|
-
# hash.transform_keys { |key| key.to_s.camelize(:lower) }
|
|
85
|
-
# end
|
|
86
|
-
|
|
87
|
-
# def titleize_keys(hash)
|
|
88
|
-
# hash.transform_keys { |key| key.to_s.titleize.delete(" ") }
|
|
89
|
-
# end
|
|
90
|
-
|
|
91
|
-
# def optionchain_api?(path)
|
|
92
|
-
# path.include?("/optionchain")
|
|
93
|
-
# end
|
|
94
72
|
end
|
|
95
73
|
end
|
|
@@ -55,6 +55,13 @@ module DhanHQ
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
class << self
|
|
58
|
+
##
|
|
59
|
+
# The option chain API takes TitleCase keys (e.g. "UnderlyingScripValue");
|
|
60
|
+
# see BaseModel.param_key_style.
|
|
61
|
+
def param_key_style
|
|
62
|
+
:title
|
|
63
|
+
end
|
|
64
|
+
|
|
58
65
|
##
|
|
59
66
|
# Provides a shared instance of the OptionChain resource.
|
|
60
67
|
#
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -491,12 +491,12 @@ module DhanHQ
|
|
|
491
491
|
# order = DhanHQ::Models::Order.find("112111182045")
|
|
492
492
|
# order.modify(validity: "IOC")
|
|
493
493
|
#
|
|
494
|
-
# @raise [
|
|
494
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
495
495
|
# @raise [DhanHQ::ValidationError] If validation fails for any parameter
|
|
496
496
|
# @raise [DhanHQ::ModificationLimitError] If this instance has already been modified 25 times (Dhan API cap)
|
|
497
497
|
# @note Count is per Order instance in this process; a fresh find() resets it.
|
|
498
498
|
def modify(new_params)
|
|
499
|
-
raise "Order ID is required to modify an order" unless id
|
|
499
|
+
raise DhanHQ::OrderError, "Order ID is required to modify an order" unless id
|
|
500
500
|
|
|
501
501
|
count = @modification_count || 0
|
|
502
502
|
if count >= Constants::RateLimit::ORDER_MODIFICATIONS_PER_ORDER
|
|
@@ -537,9 +537,9 @@ module DhanHQ
|
|
|
537
537
|
# puts "Failed to cancel order"
|
|
538
538
|
# end
|
|
539
539
|
#
|
|
540
|
-
# @raise [
|
|
540
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
541
541
|
def cancel
|
|
542
|
-
raise "Order ID is required to cancel an order" unless id
|
|
542
|
+
raise DhanHQ::OrderError, "Order ID is required to cancel an order" unless id
|
|
543
543
|
|
|
544
544
|
DhanHQ.logger&.info("[DhanHQ::Models::Order] Cancelling order #{id}")
|
|
545
545
|
response = self.class.resource.cancel(id)
|
|
@@ -566,9 +566,9 @@ module DhanHQ
|
|
|
566
566
|
# puts "Updated status: #{order.order_status}"
|
|
567
567
|
# puts "Filled: #{order.filled_qty}/#{order.quantity}"
|
|
568
568
|
#
|
|
569
|
-
# @raise [
|
|
569
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
570
570
|
def refresh
|
|
571
|
-
raise "Order ID is required to refresh an order" unless id
|
|
571
|
+
raise DhanHQ::OrderError, "Order ID is required to refresh an order" unless id
|
|
572
572
|
|
|
573
573
|
self.class.find(id)
|
|
574
574
|
end
|
|
@@ -603,7 +603,7 @@ module DhanHQ
|
|
|
603
603
|
alias delete destroy
|
|
604
604
|
|
|
605
605
|
def slice_order(params)
|
|
606
|
-
raise "Order ID is required to slice an order" unless id
|
|
606
|
+
raise DhanHQ::OrderError, "Order ID is required to slice an order" unless id
|
|
607
607
|
|
|
608
608
|
normalized = snake_case(params)
|
|
609
609
|
normalized[:dhan_client_id] ||= DhanHQ.configuration&.client_id
|