DhanHQ 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +23 -0
  3. data/CHANGELOG.md +26 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
  6. data/lib/DhanHQ/client.rb +53 -34
  7. data/lib/DhanHQ/models/alert_order.rb +1 -1
  8. data/lib/DhanHQ/risk/pipeline.rb +0 -2
  9. data/lib/DhanHQ/version.rb +1 -1
  10. data/lib/dhan_hq.rb +4 -0
  11. data/skills/dhanhq-ruby/SKILL.md +208 -0
  12. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  13. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  14. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  15. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  16. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  17. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  18. data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
  19. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  20. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  21. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  22. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  23. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  24. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  25. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  26. data/skills/dhanhq-ruby/references/funds.md +67 -0
  27. data/skills/dhanhq-ruby/references/instruments.md +91 -0
  28. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  29. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  30. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  31. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  32. data/skills/dhanhq-ruby/references/orders.md +203 -0
  33. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  34. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  35. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  36. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  37. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  38. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  39. metadata +32 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 474564a108e478d3e2cc7bb0ad2b313a885b265ca3ece5091bd04dc1bb31b014
4
- data.tar.gz: ab2a03010eb2ca03f7e6c9a01011b1b4b84f805611f22c55f7c2c07af1a7e063
3
+ metadata.gz: 287ad8c77adb3623574c0ee14102cfc3e88695168f75e1550a25dbbff9eb8508
4
+ data.tar.gz: a61b3f8e664f45f203b1afc3f840c9ed62a4e3c40e2f2d17fedc080ac682e029
5
5
  SHA512:
6
- metadata.gz: 531c7d4fba8e753c2371569a47562ed32cd026c8a2828e28bb7b617f5f342bec870ace4fac6f95b4e122b360c164c42a65a34a90899e2e100b18e7cc594ffea0
7
- data.tar.gz: 82332825d24456beeb67b61836472c05047ff323c3826f2f8324ac06ae036c10e9324788a43e1e7dbdcc74fb60cdbc385aaf373d99d851bb6732c51f6bf7638b
6
+ metadata.gz: 2c9ad1a3df906b585b0308809fb75235cf475a23c4eaddfea3f699de2e577819a1b78315b60012b6c51bb6c398dcaa35daab31d9ae9452d81d84e3e76741f0a9
7
+ data.tar.gz: 888e390276144b239a1bdd43afb9e77b58d21703587b898497b2426b8b78b47c7e03fd607bb3306fea49f9c2ae0e04a11ccb1b6968fc109fe61a8d2627b750a4
data/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ # AGENTS.md
2
+
3
+ ## Cursor Cloud specific instructions
4
+
5
+ This is a pure-Ruby gem (no Rails, no running servers). Development commands are documented in `CLAUDE.md`.
6
+
7
+ ### Quick reference
8
+
9
+ | Task | Command |
10
+ |------|---------|
11
+ | Install deps | `bundle install` |
12
+ | Tests | `bundle exec rspec` |
13
+ | Lint | `bundle exec rubocop` |
14
+ | Both | `bundle exec rake` |
15
+ | Single spec | `bundle exec rspec spec/path/to_spec.rb` |
16
+
17
+ ### Non-obvious caveats
18
+
19
+ - **Bundler 4.x required**: The lockfile was bundled with Bundler 4.0.6. Install with `sudo gem install bundler -v 4.0.6` if missing.
20
+ - **`libyaml-dev` must be installed**: The `psych` gem (transitive dep) needs `yaml.h`. Without `libyaml-dev`, `bundle install` fails on native extension build.
21
+ - **`vendor/bundle` path**: Gems are installed to `./vendor/bundle` via `.bundle/config` to avoid needing root write access to `/var/lib/gems`.
22
+ - **No real API calls in tests**: All HTTP interactions are stubbed with WebMock/VCR. No `DHAN_CLIENT_ID` or `DHAN_ACCESS_TOKEN` secrets are needed to run the test suite.
23
+ - **No services to start**: This is a library gem. There is no web server, database, or background worker. Testing is purely `bundle exec rspec` / `bundle exec rubocop`.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## [3.2.1] - 2026-07-26
2
+
3
+ ### Fixed
4
+
5
+ - **Restored the Claude Agent Skill pack to the published gem.** Switching `spec.files` from a reject-list to an allowlist in 3.2.0 stripped 51 MB of junk (a 36 MB core dump and a 17 MB `diagram.html`), but it also dropped `skills/dhanhq-ruby/` — 28 files, 164 KB: `SKILL.md` and its trigger frontmatter, 12 reference guides, 11 examples and 4 helper scripts. That pack is product content, added deliberately in #41. No Ruby code loads it at runtime, so nothing broke and no spec noticed; users installing 3.2.0 simply stopped receiving it. `CODE_OF_CONDUCT.md` and `AGENTS.md` are restored alongside it.
6
+
7
+ Unaffected and still shipped throughout: the MCP server (`lib/DhanHQ/mcp/`, `exe/dhanhq-mcp`), the agent tool layer (all 32 tools), the Ruby skills classes (`lib/DhanHQ/skills/`, 11 builtin skills), the risk pipeline and the WebSocket subsystem.
8
+
9
+ Gem size: 3.1.0 was 5.9 MB, 3.2.0 was 292 KB, and this is 324 KB. The 20× reduction is real and almost entirely the core dump; the skill pack was collateral.
10
+ - **`DhanHQ::AI` was unreachable on a bare `require "dhan_hq"`.** The Zeitwerk inflector had no `ai → AI` acronym, so it looked for `DhanHQ::Ai` while `ai.rb` defines `DhanHQ::AI`; both spellings raised `NameError`. The documented `DhanHQ::AI::PromptHelpers` and `DhanHQ::AI::ContextBuilder` entry points therefore only worked if something had already loaded the file by hand, which only `mcp/server.rb` did. Found while writing the first specs for that module.
11
+
12
+ ### Added
13
+
14
+ - **`spec/dhan_hq/gem_packaging_spec.rb`** — pins both ends of what ships, since packaging has now failed in both directions. Asserts the entry point, both executables, RBS signatures, Rails initializer, MCP server, agent tool layer, Ruby skills and the Claude Skill pack are all present; asserts the known junk, any crash dump, the spec suite and development config are all absent; and caps the uncompressed payload at 3 MB so a stray binary fails loudly rather than silently adding megabytes to a release.
15
+ - First specs for `AI::PromptHelpers` (13 examples), covering portfolio summaries, the risk report's P&L arithmetic — summed across every position, counted only for open ones — and order confirmation rendering. Built on real model instances rather than doubles, because `BaseModel` defines attribute readers per instance and `instance_double` cannot verify them.
16
+ - Specs for the lazily-built connection (not opened on construction, memoised, rebuilt on a base-URL change) and for retry exhaustion (transport errors translated, SDK errors re-raised unchanged, reads still retried).
17
+
18
+ ### Changed
19
+
20
+ - **`Client#with_transient_retry` no longer duplicates its retry branch.** It had two rescue clauses running near-identical backoff-log-sleep-retry logic, differing only in what they raised once retries were spent. Collapsed into one clause, with an `exhausted_error` helper naming the difference: Faraday's transport errors are translated to `DhanHQ::NetworkError` (they are not part of this gem's hierarchy, so a caller rescuing `DhanHQ::Error` would otherwise miss them), while the gem's own errors are re-raised unchanged. Clears the `Lint/DuplicateBranch` pressure on this method.
21
+ - **`Client#initialize` no longer opens a connection.** It now establishes state and checks its one invariant; the Faraday connection is built on first use of `#connection` and rebuilt when the configured base URL changes. Behaviour is unchanged for callers — `#connection` is still public and still reflects a sandbox toggle mid-process — but constructing a client does no network setup.
22
+ - **`Naming/PredicateMethod` is now scoped by method name instead of by file.** Seven file-level exclusions replaced with `AllowedMethods: [cancel, modify, destroy]` plus `AllowBangMethods: true`.
23
+
24
+ These are commands that report whether they succeeded, not predicates — `cancel?` would read as "should I cancel?", which is worse than the name it replaces, so adding `?` aliases would have been the wrong fix. The unambiguous-failure path for them is the bang variant (`cancel!`, `modify!`). Scoping by name rather than by file also means a genuinely mis-named predicate elsewhere in those same files is still caught, and it surfaced a now-redundant inline `rubocop:disable` in `risk/pipeline.rb`.
25
+ - `AI::PromptHelpers.portfolio_summary` computed the open-position set twice (`count(&:open?)` then `select(&:open?)`); it now derives it once. Both it and `.risk_report` wrap their collections in `Array()`, so a nil from a failed fetch renders an empty section instead of raising from inside a prompt helper — `funds` was already guarded, the collections were not.
26
+
1
27
  ## [3.2.0] - 2026-07-25
2
28
 
3
29
  ### Added
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
@@ -41,14 +41,20 @@ module DhanHQ
41
41
  # @param funds [DhanHQ::Models::Funds] Funds data
42
42
  # @return [String] Portfolio summary
43
43
  def self.portfolio_summary(holdings:, positions:, funds:)
44
+ # Array() so a missing collection renders as an empty section instead of
45
+ # raising. `funds` was already guarded; these two were not, and a prompt
46
+ # helper failing hard on an empty portfolio is the wrong trade.
47
+ all_holdings = Array(holdings)
48
+ open_positions = Array(positions).select(&:open?)
49
+
44
50
  lines = ["=== Portfolio Summary ==="]
45
51
  lines << "Funds: #{funds.to_prompt}" if funds
46
52
  lines << ""
47
- lines << "Holdings (#{holdings.size}):"
48
- holdings.each { |h| lines << " #{h.to_prompt}" }
53
+ lines << "Holdings (#{all_holdings.size}):"
54
+ all_holdings.each { |holding| lines << " #{holding.to_prompt}" }
49
55
  lines << ""
50
- lines << "Open Positions (#{positions.count(&:open?)}):"
51
- positions.select(&:open?).each { |p| lines << " #{p.to_prompt}" }
56
+ lines << "Open Positions (#{open_positions.size}):"
57
+ open_positions.each { |position| lines << " #{position.to_prompt}" }
52
58
  lines.join("\n")
53
59
  end
54
60
 
@@ -95,13 +101,17 @@ module DhanHQ
95
101
  # @param risk_params [Hash] Risk parameters
96
102
  # @return [String] Risk report
97
103
  def self.risk_report(positions:, risk_params: {})
104
+ # P&L is summed across every position; only the count is restricted to open
105
+ # ones, since a closed position still contributed realised P&L today.
106
+ all_positions = Array(positions)
107
+
98
108
  lines = ["=== Risk Report ==="]
99
- total_unrealized = positions.sum { |p| p.unrealized_profit.to_f }
100
- total_realized = positions.sum { |p| p.realized_profit.to_f }
109
+ total_unrealized = all_positions.sum { |position| position.unrealized_profit.to_f }
110
+ total_realized = all_positions.sum { |position| position.realized_profit.to_f }
101
111
 
102
112
  lines << "Total Unrealized P&L: ₹#{total_unrealized.round(2)}"
103
113
  lines << "Total Realized P&L: ₹#{total_realized.round(2)}"
104
- lines << "Open Positions: #{positions.count(&:open?)}"
114
+ lines << "Open Positions: #{all_positions.count(&:open?)}"
105
115
 
106
116
  lines << "Max Drawdown: #{risk_params[:max_drawdown]}%" if risk_params[:max_drawdown]
107
117
 
data/lib/DhanHQ/client.rb CHANGED
@@ -25,31 +25,57 @@ module DhanHQ
25
25
  include DhanHQ::RequestHelper
26
26
  include DhanHQ::ResponseHelper
27
27
 
28
- # The Faraday connection object used for HTTP requests.
29
- #
30
- # @return [Faraday::Connection] The connection instance used for API requests.
31
- attr_reader :connection
28
+ # Transient failures raised by this gem, worth retrying as-is.
29
+ RETRYABLE_SDK_ERRORS = [
30
+ DhanHQ::RateLimitError,
31
+ DhanHQ::InternalServerError,
32
+ DhanHQ::NetworkError
33
+ ].freeze
34
+ private_constant :RETRYABLE_SDK_ERRORS
35
+
36
+ # Transport failures raised by Faraday. Also worth retrying, but they are not part
37
+ # of this gem's error hierarchy, so a caller rescuing DhanHQ::Error would miss
38
+ # them — they are translated on the way out. See {#exhausted_error}.
39
+ RETRYABLE_TRANSPORT_ERRORS = [
40
+ Faraday::TimeoutError,
41
+ Faraday::ConnectionFailed
42
+ ].freeze
43
+ private_constant :RETRYABLE_TRANSPORT_ERRORS
32
44
 
33
45
  attr_reader :token_manager
34
46
 
35
- # Initializes a new DhanHQ Client instance with a Faraday connection.
47
+ # Initializes a new DhanHQ Client.
48
+ #
49
+ # Establishes state and checks its one invariant; the HTTP connection is opened
50
+ # lazily on first use, so constructing a client does no network setup.
36
51
  #
37
52
  # @example Create a new client:
38
53
  # client = DhanHQ::Client.new(api_type: :order_api)
39
54
  #
40
55
  # @param api_type [Symbol] Type of API (`:order_api`, `:data_api`, `:non_trading_api`)
41
56
  # @return [DhanHQ::Client] A new client instance.
42
- # @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
57
+ # @raise [DhanHQ::Error] If the rate limiter cannot be resolved for this API type.
43
58
  def initialize(api_type:)
44
59
  DhanHQ.ensure_configuration!
45
- # Use shared rate limiter instance per API type to ensure proper coordination
60
+ # Shared per API type, so separate clients coordinate against one limit.
46
61
  @rate_limiter = RateLimiter.for(api_type)
47
62
 
48
63
  raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
64
+ end
65
+
66
+ # The Faraday connection used for HTTP requests.
67
+ #
68
+ # Built on first use and rebuilt whenever the configured base URL changes — which
69
+ # happens when sandbox mode is toggled, or when a token endpoint hands back a
70
+ # different host mid-process.
71
+ #
72
+ # @return [Faraday::Connection]
73
+ def connection
74
+ current_url = DhanHQ.configuration.base_url
75
+ return @connection if @connection && @last_base_url == current_url
49
76
 
50
- # Store initial URL to detect changes
51
- @last_base_url = DhanHQ.configuration.base_url
52
- @connection = build_connection(@last_base_url)
77
+ @last_base_url = current_url
78
+ @connection = build_connection(current_url)
53
79
  end
54
80
 
55
81
  # Sends an HTTP request to the API with automatic retry for transient errors.
@@ -66,7 +92,6 @@ module DhanHQ
66
92
 
67
93
  @token_manager&.ensure_valid_token!
68
94
  @rate_limiter.throttle!
69
- refresh_connection!
70
95
 
71
96
  # A non-idempotent write is not safely retryable: the API has no idempotency
72
97
  # key, so a request that timed out may already have reached the exchange.
@@ -99,27 +124,13 @@ module DhanHQ
99
124
  attempt = 0
100
125
  begin
101
126
  yield
102
- rescue DhanHQ::RateLimitError, DhanHQ::InternalServerError, DhanHQ::NetworkError => e
103
- attempt += 1
104
- raise if attempt > retries
105
-
106
- backoff = calculate_backoff(attempt)
107
- DhanHQ.logger&.warn(
108
- "[DhanHQ::Client] Transient error (#{e.class}), retrying in #{backoff}s (attempt #{attempt}/#{retries})"
109
- )
110
- sleep(backoff)
111
- retry
112
- rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
127
+ rescue *RETRYABLE_SDK_ERRORS, *RETRYABLE_TRANSPORT_ERRORS => e
113
128
  attempt += 1
114
- if attempt > retries
115
- raise DhanHQ::NetworkError, "Request failed: #{e.message}" if retries.zero?
116
-
117
- raise DhanHQ::NetworkError, "Request failed after #{retries} retries: #{e.message}"
118
- end
129
+ raise exhausted_error(e, retries) if attempt > retries
119
130
 
120
131
  backoff = calculate_backoff(attempt)
121
132
  DhanHQ.logger&.warn(
122
- "[DhanHQ::Client] Network error (#{e.class}), retrying in #{backoff}s (attempt #{attempt}/#{retries})"
133
+ "[DhanHQ::Client] Transient failure (#{e.class}), retrying in #{backoff}s (attempt #{attempt}/#{retries})"
123
134
  )
124
135
  sleep(backoff)
125
136
  retry
@@ -253,12 +264,20 @@ module DhanHQ
253
264
  out
254
265
  end
255
266
 
256
- def refresh_connection!
257
- current_url = DhanHQ.configuration.base_url
258
- return if @last_base_url == current_url
259
-
260
- @last_base_url = current_url
261
- @connection = build_connection(current_url)
267
+ # The exception to raise once the retries are spent.
268
+ #
269
+ # Faraday's transport errors are translated to {DhanHQ::NetworkError}, so a caller
270
+ # rescuing DhanHQ::Error sees every failure this client can produce. This gem's own
271
+ # errors already belong to that hierarchy and are re-raised unchanged.
272
+ #
273
+ # @param error [StandardError] The failure that exhausted the retries.
274
+ # @param retries [Integer] How many retries were allowed.
275
+ # @return [StandardError] The exception to raise.
276
+ def exhausted_error(error, retries)
277
+ return error unless RETRYABLE_TRANSPORT_ERRORS.any? { |klass| error.is_a?(klass) }
278
+
279
+ attempts = retries.zero? ? "" : " after #{retries} retries"
280
+ DhanHQ::NetworkError.new("Request failed#{attempts}: #{error.message}")
262
281
  end
263
282
 
264
283
  def build_connection(url)
@@ -96,7 +96,7 @@ module DhanHQ
96
96
  handle_api_response(response, success_key: new_record? ? "alertId" : nil)
97
97
  end
98
98
 
99
- def destroy # rubocop:disable Naming/PredicateMethod
99
+ def destroy
100
100
  return false if new_record?
101
101
 
102
102
  response = self.class.resource.delete(id)
@@ -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|
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.2.0"
5
+ VERSION = "3.2.1"
6
6
  end
data/lib/dhan_hq.rb CHANGED
@@ -26,6 +26,10 @@ 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",
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: dhanhq-ruby
3
+ description: >
4
+ Use when the user mentions DhanHQ, Dhan API, or wants to trade on
5
+ Indian exchanges (NSE, BSE, MCX) using Ruby. Triggers for: place, modify, or
6
+ cancel stock/F&O/commodity orders on Dhan using Ruby; fetch portfolio holdings
7
+ or positions; get live or historical market data; access option
8
+ chains with Greeks; check fund limits or margin; build any trading
9
+ automation for Indian markets; resolve NSE/BSE instrument IDs;
10
+ stream live WebSocket market feeds or order updates. Also trigger
11
+ for general questions about programmatic trading on Indian exchanges
12
+ if Dhan is the user's broker.
13
+ compatibility: >
14
+ Requires Ruby and the dhanhq gem.
15
+ Order placement, modification, and cancellation require static IP
16
+ whitelisting on Dhan. Data APIs (quotes, history, option chain,
17
+ live feed) require an active Dhan Data Plan.
18
+ ---
19
+
20
+ # DhanHQ — Indian Market Trading Skill (Ruby SDK)
21
+
22
+ Use this skill when an agent needs to write, review, or operate Ruby code using the `DhanHQ` gem.
23
+
24
+ ## Safety Rules — Always Enforce
25
+
26
+ 1. Confirm before placing live orders.
27
+ 2. Show a readable order preview before execution.
28
+ 3. Default to `LIMIT` orders unless the user explicitly wants `MARKET`.
29
+ 4. Warn when notional exceeds `Rs. 50,000`.
30
+ 5. For F&O, validate lot size before placement.
31
+ 6. Never use `CNC` or `MTF` for F&O, commodity, or currency segments.
32
+ 7. Never hardcode credentials in generated code.
33
+ 8. Ask for confirmation before modifying or cancelling orders, or performing multi-leg live execution.
34
+
35
+ ## Setup
36
+
37
+ Require the core library and configure using environment variables or a configuration block:
38
+
39
+ ```ruby
40
+ require "dhan_hq"
41
+
42
+ # Configures from environment variables: DHAN_CLIENT_ID, DHAN_ACCESS_TOKEN
43
+ DhanHQ.configure_with_env
44
+ ```
45
+
46
+ Or configure explicitly:
47
+
48
+ ```ruby
49
+ DhanHQ.configure do |config|
50
+ config.client_id = "YOUR_CLIENT_ID"
51
+ config.access_token = "YOUR_ACCESS_TOKEN"
52
+ end
53
+ ```
54
+
55
+ If generating scripts inside this repo, prefer using the helper script:
56
+
57
+ ```ruby
58
+ require_relative "scripts/dhan_helpers"
59
+ get_client
60
+ ```
61
+
62
+ ## Current SDK Constants
63
+
64
+ Use constants from the `DhanHQ::Constants` module for segments, order types, validity, product types, and transactions:
65
+
66
+ | Category | Constant | Value |
67
+ |----------|----------|-------|
68
+ | Exchange Segment | `DhanHQ::Constants::ExchangeSegment::NSE_EQ` (or `DhanHQ::Constants::NSE`) | `"NSE_EQ"` |
69
+ | | `DhanHQ::Constants::ExchangeSegment::BSE_EQ` (or `DhanHQ::Constants::BSE`) | `"BSE_EQ"` |
70
+ | | `DhanHQ::Constants::ExchangeSegment::NSE_FNO` (or `DhanHQ::Constants::NSE_FNO` / `DhanHQ::Constants::FNO`) | `"NSE_FNO"` |
71
+ | | `DhanHQ::Constants::ExchangeSegment::BSE_FNO` (or `DhanHQ::Constants::BSE_FNO`) | `"BSE_FNO"` |
72
+ | | `DhanHQ::Constants::ExchangeSegment::MCX_COMM` (or `DhanHQ::Constants::MCX`) | `"MCX_COMM"` |
73
+ | | `DhanHQ::Constants::ExchangeSegment::IDX_I` (or `DhanHQ::Constants::INDEX`) | `"IDX_I"` |
74
+ | Transaction Type | `DhanHQ::Constants::TransactionType::BUY` (or `DhanHQ::Constants::BUY`) | `"BUY"` |
75
+ | | `DhanHQ::Constants::TransactionType::SELL` (or `DhanHQ::Constants::SELL`) | `"SELL"` |
76
+ | Order Type | `DhanHQ::Constants::OrderType::LIMIT` (or `DhanHQ::Constants::LIMIT`) | `"LIMIT"` |
77
+ | | `DhanHQ::Constants::OrderType::MARKET` (or `DhanHQ::Constants::MARKET`) | `"MARKET"` |
78
+ | | `DhanHQ::Constants::OrderType::STOP_LOSS` (or `DhanHQ::Constants::SL`) | `"STOP_LOSS"` |
79
+ | | `DhanHQ::Constants::OrderType::STOP_LOSS_MARKET` (or `DhanHQ::Constants::SLM`) | `"STOP_LOSS_MARKET"` |
80
+ | Product Type | `DhanHQ::Constants::ProductType::CNC` (or `DhanHQ::Constants::CNC`) | `"CNC"` |
81
+ | | `DhanHQ::Constants::ProductType::INTRADAY` (or `DhanHQ::Constants::INTRA`) | `"INTRADAY"` |
82
+ | | `DhanHQ::Constants::ProductType::MARGIN` (or `DhanHQ::Constants::MARGIN`) | `"MARGIN"` |
83
+ | | `DhanHQ::Constants::ProductType::MTF` (or `DhanHQ::Constants::MTF`) | `"MTF"` |
84
+ | Validity | `DhanHQ::Constants::Validity::DAY` (or `DhanHQ::Constants::DAY`) | `"DAY"` |
85
+ | | `DhanHQ::Constants::Validity::IOC` (or `DhanHQ::Constants::IOC`) | `"IOC"` |
86
+
87
+ ## Preferred SDK Methods (ActiveRecord-Style Models)
88
+
89
+ | Task | Method |
90
+ |------|--------|
91
+ | Place order | `DhanHQ::Models::Order.place(params)` |
92
+ | List all orders | `DhanHQ::Models::Order.all` |
93
+ | Order by ID | `DhanHQ::Models::Order.find(order_id)` |
94
+ | Order by correlation ID | `DhanHQ::Models::Order.find_by_correlation(correlation_id)` |
95
+ | Today's trades | `DhanHQ::Models::Trade.today` |
96
+ | Historical trades | `DhanHQ::Models::Trade.history(from_date:, to_date:, page:)` |
97
+ | Holdings | `DhanHQ::Models::Holding.all` |
98
+ | Positions | `DhanHQ::Models::Position.all` |
99
+ | Fund limits | `DhanHQ::Models::Funds.fetch` |
100
+ | Margin calculator | `DhanHQ::Models::Margin.calculate(params)` |
101
+ | Multi-instrument margin | `DhanHQ::Models::Margin.calculate_multi(params)` |
102
+ | Daily charts | `DhanHQ::Models::HistoricalData.daily(params)` |
103
+ | Intraday charts | `DhanHQ::Models::HistoricalData.intraday(params)` |
104
+ | Option chain | `DhanHQ::Models::OptionChain.fetch(params)` |
105
+ | Expiry list | `DhanHQ::Models::OptionChain.fetch_expiry_list(params)` |
106
+ | Search instruments | `DhanHQ::Models::Instrument.search(query, options)` |
107
+ | Find specific instrument by symbol | `DhanHQ::Models::Instrument.find(exchange_segment, symbol, options)` |
108
+ | Find specific instrument by security ID | `DhanHQ::Models::Instrument.find_by_security_id(exchange_segment, security_id)` |
109
+ | Find instrument anywhere | `DhanHQ::Models::Instrument.find_anywhere(symbol, options)` |
110
+ | Super orders | `DhanHQ::Models::SuperOrder.create(params)` |
111
+ | Forever orders | `DhanHQ::Models::ForeverOrder.create(params)` |
112
+ | Live market feed | `DhanHQ::WS.connect(mode: :ticker) { |tick| ... }` |
113
+ | Live order updates | `DhanHQ::WS::Orders.connect { |update| ... }` |
114
+ | Live market depth | `DhanHQ::WS::MarketDepth.connect(symbols: [{...}]) { |depth| ... }` |
115
+
116
+ ## High-Value Gotchas
117
+
118
+ - **Return values are model instances:** Most class methods return `DhanHQ::Models` objects rather than raw HTTP hashes.
119
+ - **Instrument search is segment-specific:** `DhanHQ::Models::Instrument.by_segment(exchange_segment)` downloads the CSV for a single segment, which is more token-efficient than downloading the entire master CSV.
120
+ - **Spelling fixes in models:** Typos from the Dhan API are normalized in model attributes (e.g. `availabelBalance` is normalized to `available_balance` or `availabel_balance`).
121
+ - **Timestamps:** Timestamps returned by `HistoricalData` are automatically normalized into Ruby `Time` objects.
122
+ - **WebSocket connection management:** Use sequential connections or single connection pools to avoid 429 rate limiting. Dhan allows up to 5 concurrent WebSocket connections.
123
+
124
+ ## Core Patterns
125
+
126
+ ### 1. Check account access before data calls
127
+
128
+ ```ruby
129
+ funds = DhanHQ::Models::Funds.fetch
130
+ puts "Available Balance: Rs. #{funds.availabel_balance}"
131
+ ```
132
+
133
+ ### 2. Fetch historical data
134
+
135
+ ```ruby
136
+ candles = DhanHQ::Models::HistoricalData.daily(
137
+ security_id: "2885",
138
+ exchange_segment: "NSE_EQ",
139
+ instrument: "EQUITY",
140
+ from_date: "2024-01-01",
141
+ to_date: "2024-12-31"
142
+ )
143
+
144
+ candles.each do |candle|
145
+ puts "Date: #{candle[:timestamp].to_date}, Close: #{candle[:close]}"
146
+ end
147
+ ```
148
+
149
+ ### 3. Option-chain data
150
+
151
+ ```ruby
152
+ chain = DhanHQ::Models::OptionChain.fetch(
153
+ underlying_scrip: 13,
154
+ underlying_seg: "IDX_I",
155
+ expiry: "2025-03-27"
156
+ )
157
+
158
+ puts "Underlying spot: #{chain[:last_price]}"
159
+ chain[:strikes].each do |strike_data|
160
+ puts "Strike: #{strike_data[:strike]}, Call LTP: #{strike_data[:call][:last_price]}"
161
+ end
162
+ ```
163
+
164
+ ### 4. Margin check before live order placement
165
+
166
+ ```ruby
167
+ margin = DhanHQ::Models::Margin.calculate(
168
+ security_id: "2885",
169
+ exchange_segment: "NSE_EQ",
170
+ transaction_type: "BUY",
171
+ quantity: 10,
172
+ product_type: "CNC",
173
+ price: 2450.0
174
+ )
175
+
176
+ puts "Sufficient balance? #{margin.available_balance >= margin.total_margin}"
177
+ ```
178
+
179
+ ### 5. Live market feed
180
+
181
+ ```ruby
182
+ market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
183
+ puts "Market Tick: #{tick[:security_id]} = #{tick[:ltp]}"
184
+ end
185
+
186
+ market_client.subscribe_one(segment: "NSE_EQ", security_id: "2885")
187
+ sleep(10)
188
+ market_client.stop
189
+ ```
190
+
191
+ ## Reference Files
192
+
193
+ Refer to the documents in the references directory for focused workflows:
194
+
195
+ | Need | File |
196
+ |------|------|
197
+ | Orders, super orders, forever orders | [references/orders.md](references/orders.md) |
198
+ | Holdings, positions, eDIS | [references/portfolio.md](references/portfolio.md) |
199
+ | Daily/minute history, quotes, expired options | [references/market-data.md](references/market-data.md) |
200
+ | Option-chain usage | [references/option-chain.md](references/option-chain.md) |
201
+ | Fund limits and margin checks | [references/funds.md](references/funds.md) |
202
+ | Live feeds and depth | [references/live-feed.md](references/live-feed.md) |
203
+ | Error handling | [references/error-codes.md](references/error-codes.md) |
204
+ | Instrument resolution | [references/instruments.md](references/instruments.md) |
205
+ | Multi-step execution patterns | [references/common-workflows.md](references/common-workflows.md) |
206
+ | Options analytics | [references/options-analysis-patterns.md](references/options-analysis-patterns.md) |
207
+ | Backtesting patterns | [references/backtesting-with-dhan.md](references/backtesting-with-dhan.md) |
208
+ | Extranal data sources (RSI, PE ratios, screener) | [references/scanx-data.md](references/scanx-data.md) |