DhanHQ 3.2.0 → 3.3.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/AGENTS.md +23 -0
- data/CHANGELOG.md +74 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +16 -0
- data/README.md +54 -1
- data/docs/CONFIGURATION.md +38 -14
- data/docs/RELEASE_GUIDE.md +1 -1
- data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +53 -34
- data/lib/DhanHQ/concerns/bang_writes.rb +69 -0
- data/lib/DhanHQ/concerns/tracked_writes.rb +56 -0
- data/lib/DhanHQ/configuration.rb +20 -1
- data/lib/DhanHQ/core/base_model.rb +6 -13
- data/lib/DhanHQ/deprecation.rb +62 -0
- data/lib/DhanHQ/models/alert_order.rb +8 -1
- data/lib/DhanHQ/models/forever_order.rb +9 -0
- data/lib/DhanHQ/models/global_stocks/order.rb +9 -0
- data/lib/DhanHQ/models/iceberg_order.rb +9 -0
- data/lib/DhanHQ/models/multi_order.rb +7 -0
- data/lib/DhanHQ/models/order.rb +28 -0
- data/lib/DhanHQ/models/pnl_exit.rb +7 -0
- data/lib/DhanHQ/models/super_order.rb +9 -0
- data/lib/DhanHQ/models/twap_order.rb +9 -0
- data/lib/DhanHQ/risk/pipeline.rb +0 -2
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/write_result.rb +163 -0
- data/lib/dhan_hq.rb +9 -0
- data/skills/dhanhq-ruby/SKILL.md +208 -0
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
- data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
- data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
- data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
- data/skills/dhanhq-ruby/references/error-codes.md +50 -0
- data/skills/dhanhq-ruby/references/funds.md +67 -0
- data/skills/dhanhq-ruby/references/instruments.md +91 -0
- data/skills/dhanhq-ruby/references/live-feed.md +83 -0
- data/skills/dhanhq-ruby/references/market-data.md +119 -0
- data/skills/dhanhq-ruby/references/option-chain.md +71 -0
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
- data/skills/dhanhq-ruby/references/orders.md +203 -0
- data/skills/dhanhq-ruby/references/portfolio.md +100 -0
- data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
- data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
- metadata +36 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../write_result"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Concerns
|
|
7
|
+
# Generates bang variants of write methods that raise instead of returning a falsy
|
|
8
|
+
# value or an {DhanHQ::ErrorObject}.
|
|
9
|
+
#
|
|
10
|
+
# Each generated method calls its non-bang counterpart and passes the result through
|
|
11
|
+
# {DhanHQ::WriteResult.unwrap!}, so the two cannot drift: there is no duplicated
|
|
12
|
+
# request building, validation or logging. The non-bang methods are untouched, which
|
|
13
|
+
# is what makes this additive — existing callers keep the return values they were
|
|
14
|
+
# written against.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class Order < BaseModel
|
|
18
|
+
# extend DhanHQ::Concerns::BangWrites
|
|
19
|
+
#
|
|
20
|
+
# bang_class_writes :place # defines Order.place!
|
|
21
|
+
# bang_writes :modify, :cancel # defines #modify! and #cancel!
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Order.place!(params) # => Order, or raises DhanHQ::OrderError
|
|
25
|
+
# order.cancel! # => true, or raises DhanHQ::OrderError
|
|
26
|
+
module BangWrites
|
|
27
|
+
# Defines `<name>!` instance methods.
|
|
28
|
+
#
|
|
29
|
+
# @param names [Array<Symbol>] Existing instance write methods to wrap.
|
|
30
|
+
# @param error_class [Class] Exception the generated methods raise.
|
|
31
|
+
# @return [void]
|
|
32
|
+
def bang_writes(*names, error_class: DhanHQ::OrderError)
|
|
33
|
+
names.each { |name| include bang_write_module(name, error_class) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Defines `<name>!` class methods.
|
|
37
|
+
#
|
|
38
|
+
# @param names [Array<Symbol>] Existing class write methods to wrap.
|
|
39
|
+
# @param error_class [Class] Exception the generated methods raise.
|
|
40
|
+
# @return [void]
|
|
41
|
+
def bang_class_writes(*names, error_class: DhanHQ::OrderError)
|
|
42
|
+
names.each { |name| singleton_class.include bang_write_module(name, error_class) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Built as a module rather than defined directly, so a hand-written `place!` in
|
|
48
|
+
# the class body still overrides the generated one and can call `super`.
|
|
49
|
+
def bang_write_module(name, error_class)
|
|
50
|
+
Module.new do
|
|
51
|
+
define_method(:"#{name}!") do |*args, **kwargs, &block|
|
|
52
|
+
# A caller already using the bang variant does not need a deprecation
|
|
53
|
+
# notice telling them to use the bang variant.
|
|
54
|
+
result = DhanHQ::WriteResult.suppressing_deprecation do
|
|
55
|
+
public_send(name, *args, **kwargs, &block)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
DhanHQ::WriteResult.unwrap!(
|
|
59
|
+
result,
|
|
60
|
+
operation: DhanHQ::WriteResult.operation_label(self, name),
|
|
61
|
+
error_class: error_class,
|
|
62
|
+
errors: DhanHQ::WriteResult.errors_from(self)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../write_result"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Concerns
|
|
7
|
+
# Observes non-bang write methods and reports, once per call site, when one returns
|
|
8
|
+
# a failure shape that 4.0.0 will change.
|
|
9
|
+
#
|
|
10
|
+
# Uses +prepend+ rather than +include+ because these methods are defined directly on
|
|
11
|
+
# the model classes: an included module sits behind the class in the ancestor chain
|
|
12
|
+
# and would never be reached. A prepended one sits in front, so +super+ runs the real
|
|
13
|
+
# method and the result passes through untouched.
|
|
14
|
+
#
|
|
15
|
+
# This layer only observes. It never changes a return value, never raises, and goes
|
|
16
|
+
# quiet once a call site has been migrated to the bang variant.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# class Order < BaseModel
|
|
20
|
+
# extend DhanHQ::Concerns::TrackedWrites
|
|
21
|
+
#
|
|
22
|
+
# track_class_writes :place # warns when Order.place returns nil
|
|
23
|
+
# track_writes :modify, :cancel # warns when #cancel returns false
|
|
24
|
+
# end
|
|
25
|
+
module TrackedWrites
|
|
26
|
+
# Observes `<name>` instance methods.
|
|
27
|
+
#
|
|
28
|
+
# @param names [Array<Symbol>] Existing instance write methods to observe.
|
|
29
|
+
# @return [void]
|
|
30
|
+
def track_writes(*names)
|
|
31
|
+
names.each { |name| prepend tracking_module(name) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Observes `<name>` class methods.
|
|
35
|
+
#
|
|
36
|
+
# @param names [Array<Symbol>] Existing class write methods to observe.
|
|
37
|
+
# @return [void]
|
|
38
|
+
def track_class_writes(*names)
|
|
39
|
+
names.each { |name| singleton_class.prepend tracking_module(name) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def tracking_module(name)
|
|
45
|
+
Module.new do
|
|
46
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
47
|
+
DhanHQ::WriteResult.report_ambiguous_failure(
|
|
48
|
+
super(*args, **kwargs, &block),
|
|
49
|
+
operation: DhanHQ::WriteResult.operation_label(self, name)
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -60,7 +60,7 @@ module DhanHQ
|
|
|
60
60
|
# /v2/orders that times out may well have reached the exchange, and retrying it
|
|
61
61
|
# can place a second, duplicate order. With this off, the transient error is
|
|
62
62
|
# raised to the caller, who can reconcile using the correlation id (see
|
|
63
|
-
# {#auto_correlation_id} and +DhanHQ::Models::Order.
|
|
63
|
+
# {#auto_correlation_id} and +DhanHQ::Models::Order.find_by_correlation+)
|
|
64
64
|
# before deciding to resubmit.
|
|
65
65
|
#
|
|
66
66
|
# Read requests are always retried regardless of this setting.
|
|
@@ -69,6 +69,19 @@ module DhanHQ
|
|
|
69
69
|
# @return [Boolean]
|
|
70
70
|
attr_accessor :retry_non_idempotent_writes
|
|
71
71
|
|
|
72
|
+
# Whether to log a deprecation notice, once per call site, when a non-bang write
|
|
73
|
+
# method reports failure through +nil+, +false+ or a {DhanHQ::ErrorObject}.
|
|
74
|
+
#
|
|
75
|
+
# Those contracts disagree today and will unify on {DhanHQ::ErrorObject} in 4.0.0,
|
|
76
|
+
# which is truthy — so an `if result` failure branch written against +nil+ or
|
|
77
|
+
# +false+ will silently invert. The notice names the call sites that need moving to
|
|
78
|
+
# a bang variant before then.
|
|
79
|
+
#
|
|
80
|
+
# Defaults to +true+: a notice nobody sees finds nothing. Set to +false+ once the
|
|
81
|
+
# call sites are migrated, or via +DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false+.
|
|
82
|
+
# @return [Boolean]
|
|
83
|
+
attr_accessor :warn_on_ambiguous_write_failure
|
|
84
|
+
|
|
72
85
|
# Whether to generate a +correlationId+ for order placements that do not carry
|
|
73
86
|
# one. The correlation id is the only way to answer "did my order actually go
|
|
74
87
|
# through?" after a timeout, via GET /v2/orders/external/{correlation-id}.
|
|
@@ -175,6 +188,11 @@ module DhanHQ
|
|
|
175
188
|
@retry_non_idempotent_writes == true
|
|
176
189
|
end
|
|
177
190
|
|
|
191
|
+
# @return [Boolean] True when ambiguous write failures should be reported.
|
|
192
|
+
def warn_on_ambiguous_write_failure?
|
|
193
|
+
@warn_on_ambiguous_write_failure == true
|
|
194
|
+
end
|
|
195
|
+
|
|
178
196
|
# @return [Boolean] True when a correlation id should be generated for orders.
|
|
179
197
|
def auto_correlation_id?
|
|
180
198
|
@auto_correlation_id == true
|
|
@@ -193,6 +211,7 @@ module DhanHQ
|
|
|
193
211
|
@dry_run = env_flag("DHAN_DRY_RUN", default: false)
|
|
194
212
|
@retry_non_idempotent_writes = env_flag("DHAN_RETRY_WRITES", default: false)
|
|
195
213
|
@auto_correlation_id = env_flag("DHAN_AUTO_CORRELATION_ID", default: false)
|
|
214
|
+
@warn_on_ambiguous_write_failure = env_flag("DHAN_WARN_AMBIGUOUS_WRITE_FAILURE", default: true)
|
|
196
215
|
@base_url = ENV.fetch("DHAN_BASE_URL", nil)
|
|
197
216
|
@ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
|
198
217
|
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
|
|
@@ -193,19 +193,12 @@ module DhanHQ
|
|
|
193
193
|
# @return [DhanHQ::BaseModel]
|
|
194
194
|
# @raise [DhanHQ::Error] When the record cannot be saved.
|
|
195
195
|
def save!
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
elsif @errors && !@errors.empty?
|
|
203
|
-
@errors
|
|
204
|
-
else
|
|
205
|
-
"Unknown error"
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
raise DhanHQ::Error, "Failed to save the record: #{error_details}"
|
|
196
|
+
DhanHQ::WriteResult.unwrap!(
|
|
197
|
+
save,
|
|
198
|
+
operation: "#{self.class}#save",
|
|
199
|
+
error_class: DhanHQ::Error,
|
|
200
|
+
errors: @errors
|
|
201
|
+
)
|
|
209
202
|
end
|
|
210
203
|
|
|
211
204
|
# Delete the resource
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Emits a deprecation notice once per call site per process.
|
|
5
|
+
#
|
|
6
|
+
# A trading process can reject hundreds of orders in a session. A notice that fires
|
|
7
|
+
# on every one of them is noise that gets filtered out, which defeats the purpose —
|
|
8
|
+
# the point is for a maintainer to find the handful of call sites still relying on
|
|
9
|
+
# behaviour that is going to change, then fix them.
|
|
10
|
+
module Deprecation
|
|
11
|
+
@warned = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Logs +message+ the first time this +key+ is seen in this process.
|
|
16
|
+
#
|
|
17
|
+
# A command, not a query: callers that need to know whether a notice was emitted
|
|
18
|
+
# should consult {.warned_keys}.
|
|
19
|
+
#
|
|
20
|
+
# @param key [String, Symbol] Identifies the call site, e.g. the operation name.
|
|
21
|
+
# @param message [String] What is deprecated and what to do instead.
|
|
22
|
+
# @return [void]
|
|
23
|
+
def warn_once(key, message)
|
|
24
|
+
return unless first_warning_for?(key)
|
|
25
|
+
|
|
26
|
+
DhanHQ.logger&.warn("[DhanHQ] DEPRECATION: #{message}")
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Keys already warned about, for assertions and diagnostics.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<String>]
|
|
33
|
+
def warned_keys
|
|
34
|
+
@mutex.synchronize { @warned.keys }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Forgets every emitted notice, so the next occurrence warns again.
|
|
38
|
+
# Intended for test isolation.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def reset!
|
|
42
|
+
@mutex.synchronize { @warned.clear }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Whether this is the first time +key+ has been seen, recording it either way.
|
|
48
|
+
#
|
|
49
|
+
# The check and the record happen under one lock so two threads hitting the same
|
|
50
|
+
# call site at once cannot both decide they are first.
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def first_warning_for?(key)
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
next false if @warned.key?(key.to_s)
|
|
56
|
+
|
|
57
|
+
@warned[key.to_s] = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -6,6 +6,13 @@ module DhanHQ
|
|
|
6
6
|
module Models
|
|
7
7
|
# Model for alert/conditional orders. CRUD via AlertOrders resource; validated by AlertOrderContract.
|
|
8
8
|
class AlertOrder < BaseModel
|
|
9
|
+
extend DhanHQ::Concerns::BangWrites
|
|
10
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
11
|
+
|
|
12
|
+
track_class_writes :create, :modify
|
|
13
|
+
|
|
14
|
+
bang_class_writes :create, :modify
|
|
15
|
+
|
|
9
16
|
include Concerns::ApiResponseHandler
|
|
10
17
|
|
|
11
18
|
HTTP_PATH = "/v2/alerts/orders"
|
|
@@ -96,7 +103,7 @@ module DhanHQ
|
|
|
96
103
|
handle_api_response(response, success_key: new_record? ? "alertId" : nil)
|
|
97
104
|
end
|
|
98
105
|
|
|
99
|
-
def destroy
|
|
106
|
+
def destroy
|
|
100
107
|
return false if new_record?
|
|
101
108
|
|
|
102
109
|
response = self.class.resource.delete(id)
|
|
@@ -66,6 +66,15 @@ module DhanHQ
|
|
|
66
66
|
# orders.each { |order| puts order.order_status }
|
|
67
67
|
#
|
|
68
68
|
class ForeverOrder < BaseModel
|
|
69
|
+
extend DhanHQ::Concerns::BangWrites
|
|
70
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
71
|
+
|
|
72
|
+
track_class_writes :create
|
|
73
|
+
track_writes :modify, :cancel
|
|
74
|
+
|
|
75
|
+
bang_class_writes :create
|
|
76
|
+
bang_writes :modify, :cancel
|
|
77
|
+
|
|
69
78
|
include Concerns::ApiResponseHandler
|
|
70
79
|
|
|
71
80
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
@@ -36,6 +36,15 @@ module DhanHQ
|
|
|
36
36
|
# end
|
|
37
37
|
#
|
|
38
38
|
class Order < BaseModel
|
|
39
|
+
extend DhanHQ::Concerns::BangWrites
|
|
40
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
41
|
+
|
|
42
|
+
track_class_writes :place
|
|
43
|
+
track_writes :modify, :cancel
|
|
44
|
+
|
|
45
|
+
bang_class_writes :place
|
|
46
|
+
bang_writes :modify, :cancel
|
|
47
|
+
|
|
39
48
|
HTTP_PATH = "/v2/globalstocks/orders"
|
|
40
49
|
|
|
41
50
|
attributes :dhan_client_id, :order_id, :exchange_order_id, :correlation_id,
|
|
@@ -48,6 +48,15 @@ module DhanHQ
|
|
|
48
48
|
# orders = DhanHQ::Models::IcebergOrder.all
|
|
49
49
|
#
|
|
50
50
|
class IcebergOrder < BaseModel
|
|
51
|
+
extend DhanHQ::Concerns::BangWrites
|
|
52
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
53
|
+
|
|
54
|
+
track_class_writes :create
|
|
55
|
+
track_writes :modify, :cancel
|
|
56
|
+
|
|
57
|
+
bang_class_writes :create
|
|
58
|
+
bang_writes :modify, :cancel
|
|
59
|
+
|
|
51
60
|
include Concerns::ApiResponseHandler
|
|
52
61
|
|
|
53
62
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
@@ -26,6 +26,13 @@ module DhanHQ
|
|
|
26
26
|
# result.for_sequence("1").order_id
|
|
27
27
|
#
|
|
28
28
|
class MultiOrder < BaseModel
|
|
29
|
+
extend DhanHQ::Concerns::BangWrites
|
|
30
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
31
|
+
|
|
32
|
+
track_class_writes :place
|
|
33
|
+
|
|
34
|
+
bang_class_writes :place
|
|
35
|
+
|
|
29
36
|
HTTP_PATH = "/v2/alerts/multi/orders"
|
|
30
37
|
|
|
31
38
|
attributes :orders
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -46,6 +46,15 @@ module DhanHQ
|
|
|
46
46
|
# puts "Pending orders: #{pending_orders.count}"
|
|
47
47
|
#
|
|
48
48
|
class Order < BaseModel
|
|
49
|
+
extend DhanHQ::Concerns::BangWrites
|
|
50
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
51
|
+
|
|
52
|
+
track_class_writes :place, :create
|
|
53
|
+
track_writes :modify, :cancel
|
|
54
|
+
|
|
55
|
+
bang_class_writes :place, :create
|
|
56
|
+
bang_writes :modify, :cancel, :refresh
|
|
57
|
+
|
|
49
58
|
include Concerns::ApiResponseHandler
|
|
50
59
|
|
|
51
60
|
# Attributes eligible for modification requests.
|
|
@@ -430,6 +439,25 @@ module DhanHQ
|
|
|
430
439
|
order.save # calls resource create or update
|
|
431
440
|
order
|
|
432
441
|
end
|
|
442
|
+
|
|
443
|
+
# `create` always returns the built `Order`, even when `#save` returned
|
|
444
|
+
# `false` -- existing callers rely on that to inspect an unsaved order's
|
|
445
|
+
# `errors`, so `create` cannot change. The generated `create!` would
|
|
446
|
+
# therefore call `unwrap!` on an object that is truthy regardless of
|
|
447
|
+
# whether the placement actually succeeded, and never raise. This
|
|
448
|
+
# hand-written override supersedes it (see {BangWrites} for why a
|
|
449
|
+
# class-body definition takes precedence) and unwraps on `persisted?`
|
|
450
|
+
# instead, which reflects whether the API actually accepted the order.
|
|
451
|
+
def create!(params)
|
|
452
|
+
order = DhanHQ::WriteResult.suppressing_deprecation { create(params) }
|
|
453
|
+
|
|
454
|
+
DhanHQ::WriteResult.unwrap!(
|
|
455
|
+
order.persisted? ? order : false,
|
|
456
|
+
operation: DhanHQ::WriteResult.operation_label(self, :create),
|
|
457
|
+
error_class: DhanHQ::OrderError,
|
|
458
|
+
errors: DhanHQ::WriteResult.errors_from(order)
|
|
459
|
+
)
|
|
460
|
+
end
|
|
433
461
|
end
|
|
434
462
|
|
|
435
463
|
##
|
|
@@ -32,6 +32,13 @@ module DhanHQ
|
|
|
32
32
|
# puts response[:pnl_exit_status] # => "DISABLED"
|
|
33
33
|
#
|
|
34
34
|
class PnlExit < BaseModel
|
|
35
|
+
extend DhanHQ::Concerns::BangWrites
|
|
36
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
37
|
+
|
|
38
|
+
track_class_writes :configure, :stop
|
|
39
|
+
|
|
40
|
+
bang_class_writes :configure, :stop
|
|
41
|
+
|
|
35
42
|
HTTP_PATH = "/v2/pnlExit"
|
|
36
43
|
|
|
37
44
|
attributes :pnl_exit_status, :profit, :loss, :segments, :enable_kill_switch
|
|
@@ -48,6 +48,15 @@ module DhanHQ
|
|
|
48
48
|
# order.cancel("STOP_LOSS_LEG")
|
|
49
49
|
#
|
|
50
50
|
class SuperOrder < BaseModel
|
|
51
|
+
extend DhanHQ::Concerns::BangWrites
|
|
52
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
53
|
+
|
|
54
|
+
track_class_writes :create
|
|
55
|
+
track_writes :modify, :cancel
|
|
56
|
+
|
|
57
|
+
bang_class_writes :create
|
|
58
|
+
bang_writes :modify, :cancel
|
|
59
|
+
|
|
51
60
|
include Concerns::ApiResponseHandler
|
|
52
61
|
|
|
53
62
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
@@ -45,6 +45,15 @@ module DhanHQ
|
|
|
45
45
|
# order.cancel
|
|
46
46
|
#
|
|
47
47
|
class TwapOrder < BaseModel
|
|
48
|
+
extend DhanHQ::Concerns::BangWrites
|
|
49
|
+
extend DhanHQ::Concerns::TrackedWrites
|
|
50
|
+
|
|
51
|
+
track_class_writes :create
|
|
52
|
+
track_writes :modify, :cancel
|
|
53
|
+
|
|
54
|
+
bang_class_writes :create
|
|
55
|
+
bang_writes :modify, :cancel
|
|
56
|
+
|
|
48
57
|
include Concerns::ApiResponseHandler
|
|
49
58
|
|
|
50
59
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
data/lib/DhanHQ/risk/pipeline.rb
CHANGED
|
@@ -51,14 +51,12 @@ module DhanHQ
|
|
|
51
51
|
# @param type [Symbol] :equity or :options
|
|
52
52
|
# @return [true] if all checks pass
|
|
53
53
|
# @raise [DhanHQ::RiskViolation] on first failure
|
|
54
|
-
# rubocop:disable Naming/PredicateMethod
|
|
55
54
|
def self.run!(instrument:, args:, now: Time.now, type: :equity)
|
|
56
55
|
run_checks!(CHECKS, instrument, args, now)
|
|
57
56
|
run_checks!(OPTION_CHECKS, instrument, args, now) if type == :options
|
|
58
57
|
run_checks!(DAILY_CHECKS, instrument, args, now)
|
|
59
58
|
true
|
|
60
59
|
end
|
|
61
|
-
# rubocop:enable Naming/PredicateMethod
|
|
62
60
|
|
|
63
61
|
def self.run_checks!(checks, instrument, args, now)
|
|
64
62
|
checks.each do |check|
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Interprets the several shapes a write currently uses to signal failure.
|
|
5
|
+
#
|
|
6
|
+
# The model write methods do not share a return contract. Depending on which class
|
|
7
|
+
# and which failure you hit, a rejected write comes back as +nil+, as +false+, or as
|
|
8
|
+
# a {DhanHQ::ErrorObject} — and {DhanHQ::Models::AlertOrder.modify} can return either
|
|
9
|
+
# of the first two from the same method. A caller cannot write one error branch.
|
|
10
|
+
#
|
|
11
|
+
# Unifying those contracts is a breaking change for the applications that depend on
|
|
12
|
+
# this gem, so it is staged (see CHANGELOG). This module is step one: it puts the
|
|
13
|
+
# knowledge of "what does failure look like" in exactly one place, and backs the bang
|
|
14
|
+
# variants (+place!+, +modify!+, +cancel!+, …) that give callers a single, explicit
|
|
15
|
+
# failure mode to rescue today.
|
|
16
|
+
#
|
|
17
|
+
# @example Opting a call site into explicit failures
|
|
18
|
+
# order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError
|
|
19
|
+
# # instead of
|
|
20
|
+
# order = DhanHQ::Models::Order.place(params) # nil on failure
|
|
21
|
+
module WriteResult
|
|
22
|
+
# Thread-local flag set while a bang variant is calling through.
|
|
23
|
+
SUPPRESSION_KEY = :dhanhq_suppress_write_deprecation
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Whether a write result represents a rejected or failed operation.
|
|
28
|
+
#
|
|
29
|
+
# @param result [Object] Return value of a write method.
|
|
30
|
+
# @return [Boolean]
|
|
31
|
+
def failure?(result)
|
|
32
|
+
result.nil? || result == false || result.is_a?(DhanHQ::ErrorObject)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param result [Object] Return value of a write method.
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def success?(result)
|
|
38
|
+
!failure?(result)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the result, or raises carrying whatever diagnostics the failure held.
|
|
42
|
+
#
|
|
43
|
+
# @param result [Object] Return value of a write method.
|
|
44
|
+
# @param operation [String] Human-readable operation name for the message,
|
|
45
|
+
# e.g. +"DhanHQ::Models::Order.place"+.
|
|
46
|
+
# @param error_class [Class] Exception to raise. Defaults to {DhanHQ::OrderError},
|
|
47
|
+
# which descends from {DhanHQ::Error}, so existing `rescue DhanHQ::Error`
|
|
48
|
+
# handlers still catch it.
|
|
49
|
+
# @param errors [Hash, nil] Validation errors to report when the result itself
|
|
50
|
+
# carries none — typically a model's +errors+ hash.
|
|
51
|
+
# @return [Object] The result, when it represents success.
|
|
52
|
+
# @raise [DhanHQ::Error] When the result represents failure.
|
|
53
|
+
def unwrap!(result, operation:, error_class: DhanHQ::OrderError, errors: nil)
|
|
54
|
+
return result if success?(result)
|
|
55
|
+
|
|
56
|
+
raise error_class, "#{operation} failed: #{describe(result, errors)}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Reports, once per call site, that a non-bang write signalled failure through a
|
|
60
|
+
# value whose shape is going to change in 4.0.0.
|
|
61
|
+
#
|
|
62
|
+
# Step two of the migration: the notice tells a maintainer which of their call
|
|
63
|
+
# sites still branch on the old return value, so they can move to the bang variant
|
|
64
|
+
# before the non-bang contract unifies on {DhanHQ::ErrorObject}. Returns the result
|
|
65
|
+
# untouched — this observes, it never alters behaviour.
|
|
66
|
+
#
|
|
67
|
+
# Silent when the write succeeded, when the caller opted out via
|
|
68
|
+
# +config.warn_on_ambiguous_write_failure = false+, or when reached through a bang
|
|
69
|
+
# variant (those callers have already migrated — see {.suppressing_deprecation}).
|
|
70
|
+
#
|
|
71
|
+
# @param result [Object] Return value of a non-bang write method.
|
|
72
|
+
# @param operation [String] Operation label from {.operation_label}.
|
|
73
|
+
# @return [Object] The result, unchanged.
|
|
74
|
+
def report_ambiguous_failure(result, operation:)
|
|
75
|
+
return result unless failure?(result)
|
|
76
|
+
return result if suppressed?
|
|
77
|
+
return result unless DhanHQ.configuration&.warn_on_ambiguous_write_failure?
|
|
78
|
+
|
|
79
|
+
DhanHQ::Deprecation.warn_once(
|
|
80
|
+
operation,
|
|
81
|
+
"#{operation} reported failure as #{shape_of(result)}. Write methods return " \
|
|
82
|
+
"nil, false or a DhanHQ::ErrorObject inconsistently today and will all return " \
|
|
83
|
+
"DhanHQ::ErrorObject in 4.0.0, which is truthy — an `if result` failure branch " \
|
|
84
|
+
"will invert. Use #{operation}! to get a DhanHQ::OrderError instead, or set " \
|
|
85
|
+
"config.warn_on_ambiguous_write_failure = false to silence this."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Runs the block with {.report_ambiguous_failure} disabled on this thread.
|
|
92
|
+
#
|
|
93
|
+
# Used by the bang variants: they call the non-bang method to get its result, and a
|
|
94
|
+
# caller who has already moved to `place!` does not need telling to move to
|
|
95
|
+
# `place!`. Thread-local so a concurrent thread still gets its own notices.
|
|
96
|
+
#
|
|
97
|
+
# @return [Object] The block's value.
|
|
98
|
+
def suppressing_deprecation
|
|
99
|
+
previous = Thread.current[SUPPRESSION_KEY]
|
|
100
|
+
Thread.current[SUPPRESSION_KEY] = true
|
|
101
|
+
yield
|
|
102
|
+
ensure
|
|
103
|
+
Thread.current[SUPPRESSION_KEY] = previous
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @return [Boolean]
|
|
107
|
+
def suppressed?
|
|
108
|
+
Thread.current[SUPPRESSION_KEY] == true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Names the shape a failure arrived in, for the notice.
|
|
112
|
+
#
|
|
113
|
+
# @return [String]
|
|
114
|
+
def shape_of(result)
|
|
115
|
+
return "nil" if result.nil?
|
|
116
|
+
return "false" if result == false
|
|
117
|
+
|
|
118
|
+
"a DhanHQ::ErrorObject"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Label identifying the operation that failed, for the exception message.
|
|
122
|
+
#
|
|
123
|
+
# @param receiver [Object] The class (for a class method) or instance.
|
|
124
|
+
# @param name [Symbol] Method name.
|
|
125
|
+
# @return [String] e.g. +"DhanHQ::Models::Order.place"+ or +"…Order#modify"+.
|
|
126
|
+
def operation_label(receiver, name)
|
|
127
|
+
return "#{module_label(receiver)}.#{name}" if receiver.is_a?(Module)
|
|
128
|
+
|
|
129
|
+
"#{module_label(receiver.class)}##{name}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Reads a module's declared name, falling back to +to_s+ so an anonymous class
|
|
133
|
+
# still yields something readable rather than an object address.
|
|
134
|
+
#
|
|
135
|
+
# @return [String]
|
|
136
|
+
def module_label(mod)
|
|
137
|
+
mod.name || mod.to_s
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Validation errors carried by a model instance, when it has any.
|
|
141
|
+
#
|
|
142
|
+
# @param receiver [Object]
|
|
143
|
+
# @return [Hash, nil]
|
|
144
|
+
def errors_from(receiver)
|
|
145
|
+
return nil if receiver.is_a?(Module)
|
|
146
|
+
return nil unless receiver.respond_to?(:errors)
|
|
147
|
+
|
|
148
|
+
receiver.errors
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Best available explanation for a failure.
|
|
152
|
+
#
|
|
153
|
+
# @return [String]
|
|
154
|
+
def describe(result, errors = nil)
|
|
155
|
+
return result.errors.to_s if result.is_a?(DhanHQ::ErrorObject)
|
|
156
|
+
return errors.to_s if errors && !errors.empty?
|
|
157
|
+
|
|
158
|
+
# `nil` and `false` carry nothing, so say which of the two it was rather than
|
|
159
|
+
# inventing a cause.
|
|
160
|
+
result.nil? ? "the API returned no record" : "the API rejected the request"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -26,11 +26,20 @@ module DhanHQ
|
|
|
26
26
|
LOADER = Zeitwerk::Loader.new
|
|
27
27
|
LOADER.tag = "dhanhq"
|
|
28
28
|
LOADER.inflector.inflect(
|
|
29
|
+
# Without this, Zeitwerk looks for DhanHQ::Ai while ai.rb defines DhanHQ::AI, so
|
|
30
|
+
# DhanHQ::AI::PromptHelpers raised NameError unless something had already loaded
|
|
31
|
+
# the file by hand — which only mcp/server.rb did.
|
|
32
|
+
"ai" => "AI",
|
|
29
33
|
"api_helper" => "APIHelper",
|
|
30
34
|
"auth_api" => "AuthAPI",
|
|
31
35
|
"base_api" => "BaseAPI",
|
|
32
36
|
"ip_setup" => "IPSetup",
|
|
33
37
|
"json_loader" => "JSONLoader",
|
|
38
|
+
# Same failure as "ai" above: mcp.rb defines DhanHQ::MCP while Zeitwerk expected
|
|
39
|
+
# DhanHQ::Mcp, so DhanHQ::MCP::Server raised NameError after a bare `require
|
|
40
|
+
# "dhan_hq"` — it only worked via exe/dhanhq-mcp and lib/dhan_hq/mcp.rb, which
|
|
41
|
+
# require_relative the file directly instead of going through the autoloader.
|
|
42
|
+
"mcp" => "MCP",
|
|
34
43
|
"ws" => "WS"
|
|
35
44
|
)
|
|
36
45
|
LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
|