DhanHQ 3.3.0 → 3.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 065ac08985790dc4172816430025c482f539b506445c84181f69bdb56d30f510
4
- data.tar.gz: 8f545f2ac8416dfadfe4fc0a5216b4203d06eaf05e1b1094d14ee571e84de342
3
+ metadata.gz: 6d97f7473f5bb7c4968750c3e21e5721a9c9ba1261e6e00d450818613edb74ec
4
+ data.tar.gz: 49464e4909ac0f52947fc86735b6b43bef6877f8ba0a1f796eb3b1c58c60bfde
5
5
  SHA512:
6
- metadata.gz: 27d821b96f67997c64f1a523c2a366736dabcea5175e621014e22c9b691f35835f008f3bacaefe6b26229d2de92a78d44719f1b5ebff3e288960f156f7192331
7
- data.tar.gz: 76558d36116bbc9c2b024aacc0c6cb1c30640b777cf2cd0832cb6d8da9b142fd6aca350318489a3d13f848136c1089bcd1ef014a2a975c8f66bde1e7c8131d10
6
+ metadata.gz: 5a70be1d7aa2af222df63e8188af1b60e110431d73ae36317244693d974e46a3fcb84a9a49d9276fbadeea105c1124753129aa1c057d089924f0b71c7075f481
7
+ data.tar.gz: cb8a910ad46ae08b570eb9b19ac3a553de9a050e1b2a409d1414f3eb18d7eb762cb2fc62d6f2a5a72ad9014f726a1195541fa29ea80d749ba4c1dc21d6b0903f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [3.4.0] - 2026-08-15
2
+
3
+ ### Added
4
+
5
+ - **`DhanHQ::Backtest::Runner`** (with `Trade` and `Result`) — replays a `DhanHQ::Strategy::Base` against historical `OHLCSeries` candles and returns a trade log, a per-bar equity curve, and summary stats (`total_return_pct`, `win_rate`, `max_drawdown_pct`, `num_trades`, `avg_trade_pnl`).
6
+
7
+ Both entries and exits fill at the *next* candle's open, never the signal candle's own price — deciding to act on a candle and then filling somewhere inside that same candle is look-ahead bias, since the fill price would have to come from before the candle closed. The equity curve stays flat on the signal bar itself for the same reason: a position isn't marked-to-market until it's actually been filled on the following bar. Risk-rule violations reuse `Strategy::Base#check_risks` rather than a second DSL, and an unclosed position at the end of the dataset is force-closed at the final candle's close. Optional `max_bars_held:` guards against a strategy bug holding a position across the entire dataset.
8
+
9
+ - **`QUICKSTART.md`** — the 5 most common tasks (install/config, reads, safe order placement, WS streaming, strategy building) in under 50 lines, linked from the README.
10
+
11
+ - **`DHAN_WS_DEBUG=true`** (`config.ws_debug`) — hex-dumps every raw inbound WebSocket frame at `debug` level before it's parsed, for troubleshooting binary parse errors and dead-but-connected feeds. Off by default, checked before any hex-encoding work so there's no cost when disabled, and capped at 256 bytes per frame (with a `...truncated` marker and the full byte count always logged) so a market-depth feed at tick frequency can't flood the log.
12
+
13
+ There isn't a single choke point all raw frames pass through: the market-feed `DhanHQ::WS::Connection` predates `BaseConnection` and has its own `on(:message)` handler, `DhanHQ::WS::Orders::Connection` overrides `BaseConnection#handle_message` entirely without calling `super`, and only `DhanHQ::WS::MarketDepth::Client` goes through `BaseConnection` unmodified. `DhanHQ::WS.debug_frame` is wired into all three so there's one hex-dump implementation, not three that could drift.
14
+
15
+ - **`rails generate dhanhq:install`** — scaffolds `config/initializers/dhanhq.rb`, an order-placing service object, a Sidekiq market-feed worker, and an ActionCable channel in one command.
16
+
17
+ - **`DhanHQ::Jobs::PlaceOrderJob`** — an ActiveJob wrapper around `Order.place!`. Uses `discard_on` for `DhanHQ::OrderError`/`DhanHQ::RiskViolation` rather than a manual `rescue`: `discard_on` is handled inside ActiveJob's own `execute`, before an exception would ever reach a queue adapter's own backend-level retry (Sidekiq retries unhandled exceptions by default, independent of ActiveJob's opt-in `retry_on`) — the only adapter-agnostic way to guarantee this non-idempotent write is never silently retried.
18
+
19
+ ### Fixed
20
+
21
+ - **`docs/RAILS_INTEGRATION.md`'s Sidekiq examples (§5, §6) called `client.wait!`, `client.subscribe(array)`, and `DhanHQ::WS::Client.new(kind: :order_updates)`** — none of which exist. `DhanHQ::WS::Client`/`DhanHQ::WS::Orders::Client` have no blocking wait method at all; `Client#start` spawns a background thread and returns immediately. Replaced with the real API (`DhanHQ::WS.connect`/`DhanHQ::WS::Orders.connect` plus a `connected?`-based liveness loop), guarded by a new spec that locks down the method names these examples call by name.
22
+ - **`dry-validation` had no version floor in the gemspec.** A fresh `bundle install` could resolve it to `0.4.1` — a 2016-era, pre-`Dry::Validation::Contract` API generation every contract in `lib/DhanHQ/contracts/` is incompatible with. Pinned to `~> 1.11`. Found while investigating whether the Ruby floor could drop to 3.1 (it can't — see below); confirmed by forcing an actual fresh resolve under Ruby 3.1.6, which hit exactly this.
23
+ - **`spec/dhan_hq/contracts/expired_options_data_contract_spec.rb` and `expired_options_data_spec.rb` hardcoded `from_date: "2021-08-02"`**, which is itself now more than 5 years in the past and so failed the contract's own "cannot be more than 5 years ago" rule — the shared fixture failed the exact rule it existed to exercise, cascading into every test merged onto it. Replaced every literal 2021 date with one computed relative to `Date.today`, preserving each test's original intent (span length, ordering, the exact-31-day boundary).
24
+
25
+ ### Investigated, not changed
26
+
27
+ - **Lowering `required_ruby_version` below `3.2.0`.** This gem's own code needed only two trivial fixes (anonymous `**` keyword forwarding, genuinely 3.2-only syntax — endless methods and anonymous `&` block forwarding, the original suspects, are 3.0+ and 3.1+ respectively and were never the issue). But forcing a fresh dependency resolve under Ruby 3.1.6 — after fixing the `dry-validation` pin above — still forced `activesupport` down to 7.2.3.2 (ActiveSupport 8.x itself requires Ruby ≥ 3.2), and AS 7-vs-8 behavioral differences broke `Agent::ToolRegistry`, `MCP::Server`, and `Risk::Pipeline`: 63 failures across areas with no connection to Ruby version syntax at all. That's a materially larger compatibility surface than a floor bump, so `required_ruby_version` stays `>= 3.2.0`.
28
+
1
29
  ## [3.3.0] - 2026-07-26
2
30
 
3
31
  ### Added
data/README.md CHANGED
@@ -1,31 +1,44 @@
1
- # DhanHQ — The Ruby SDK for Dhan API v2
1
+ # DhanHQ — Ruby SDK & Client for Dhan API v2
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/DhanHQ.svg)](https://rubygems.org/gems/DhanHQ)
4
4
  [![CI](https://github.com/shubhamtaywade82/dhanhq-client/actions/workflows/main.yml/badge.svg)](https://github.com/shubhamtaywade82/dhanhq-client/actions/workflows/main.yml)
5
5
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-ruby.svg)](https://www.ruby-lang.org)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.txt)
7
7
 
8
- Build trading systems in Ruby without fighting raw HTTP, fragile auth flows, or unreliable market streams.
8
+ **DhanHQ** is a production-grade **Ruby SDK for the Dhan API v2** — build algorithmic trading systems, market data pipelines, and portfolio management tools for Indian markets (NSE, BSE, MCX) with clean Ruby abstractions, resilient WebSocket streaming, typed models, dry-validation contracts, and safety-focused order workflows for Ruby on Rails and standalone Ruby applications.
9
9
 
10
- DhanHQ is a production-grade Ruby SDK for the [Dhan trading API](https://dhanhq.co/docs/v2/), designed for:
10
+ If you're looking for a Ruby gem for the Dhan trading API, this is built to be the default choice.
11
11
 
12
- - trading bots
13
- - real-time market data streaming
14
- - portfolio and order management
15
- - Rails or standalone trading systems
12
+ ## Quick Start
16
13
 
17
- If you're looking for a Ruby SDK for Dhan API, this is built to be the default choice.
14
+ ```ruby
15
+ # Gemfile
16
+ gem 'DhanHQ'
17
+ ```
18
18
 
19
- Unlike thin wrappers, DhanHQ gives you:
19
+ ```ruby
20
+ require 'dhan_hq'
21
+
22
+ DhanHQ.configure do |c|
23
+ c.client_id = ENV["DHAN_CLIENT_ID"]
24
+ c.access_token = ENV["DHAN_ACCESS_TOKEN"]
25
+ end
20
26
 
21
- - typed models for orders, positions, holdings, and more
22
- - WebSocket clients with auto-reconnect and backoff
23
- - token lifecycle management with retry-on-401
24
- - safety rails for live trading
27
+ # You're live no manual HTTP, no JSON parsing
28
+ positions = DhanHQ::Models::Position.all
29
+ ```
25
30
 
26
- This is closer to trading infrastructure than a simple API client.
31
+ ## Features
27
32
 
28
- ## Install and Run in 60 Seconds
33
+ - **Typed models** for orders, positions, holdings, funds, and trades
34
+ - **WebSocket market feed** with auto-reconnect and exponential backoff
35
+ - **WebSocket order updates** — real-time execution events
36
+ - **Token lifecycle management** with automatic retry-on-401
37
+ - **dry-validation contracts** for every trading request
38
+ - **Rails integration** with ActionCable, config generators, and rake tasks
39
+ - **Safety rails** — validation before transport, no blind retries
40
+ - **Comprehensive docs** — 25+ guides covering auth, WebSocket, orders, super orders, TA, and testing
41
+ - **REST API** — orders, super orders, positions, holdings, funds, instruments, option chain, historical data, and more
29
42
 
30
43
  ```ruby
31
44
  # Gemfile
@@ -62,7 +75,7 @@ positions = DhanHQ::Models::Position.all
62
75
 
63
76
  ## Start Here (Pick Your Use Case)
64
77
 
65
- Pick the path that matches what you want to build:
78
+ Pick the path that matches what you want to build, or just read the [Quickstart](QUICKSTART.md) top to bottom:
66
79
 
67
80
  - **Get live prices fast** → [Market Feed WebSocket](#market-feed-ticker--quote--full)
68
81
  - **Place orders safely** → [Order Safety](#order-safety)
@@ -529,6 +542,10 @@ client.health
529
542
  > 5,000 instruments per connection and 100 instruments per subscribe frame. Running several
530
543
  > strategies in separate processes counts against the same limit — a 6th connection is refused.
531
544
 
545
+ If frames stop arriving or parsing fails and `healthy?`/logs alone aren't enough, set
546
+ `DHAN_WS_DEBUG=true` to hex-dump every raw inbound frame at debug level — see
547
+ [Troubleshooting](docs/TROUBLESHOOTING.md#websocket-frame-debugging).
548
+
532
549
  ### Cleanup
533
550
 
534
551
  ```ruby
@@ -704,7 +721,11 @@ sleep # keep the script alive
704
721
 
705
722
  ## Rails Integration
706
723
 
707
- Need initializers, service objects, ActionCable wiring, and background workers? See the [Rails Integration Guide](docs/RAILS_INTEGRATION.md).
724
+ ```bash
725
+ rails generate dhanhq:install
726
+ ```
727
+
728
+ Scaffolds `config/initializers/dhanhq.rb`, an order-placing service object, a Sidekiq market-feed worker, and an ActionCable channel in one command. For the full picture — service objects, ActionCable wiring, background workers, dynamic token providers — see the [Rails Integration Guide](docs/RAILS_INTEGRATION.md).
708
729
 
709
730
  ---
710
731
 
@@ -743,6 +764,7 @@ For search-driven discovery and onboarding content, see:
743
764
 
744
765
  | Guide | What it covers |
745
766
  | ----- | -------------- |
767
+ | [Quickstart](QUICKSTART.md) | The 5 most common tasks in under 50 lines |
746
768
  | [Architecture](ARCHITECTURE.md) | Layering, dependency flow, design patterns, extension points |
747
769
  | [Authentication](docs/AUTHENTICATION.md) | Token flows, TOTP, OAuth, auto-management |
748
770
  | [Configuration Reference](docs/CONFIGURATION.md) | Full ENV matrix, logging, timeouts, available resources |
@@ -51,6 +51,7 @@ These change what a write request *does*, not just where it goes. All default to
51
51
  | `DHAN_RETRY_WRITES=true` | `config.retry_non_idempotent_writes` | `false` | Auto-retries a non-idempotent write (order placement, modify, cancel) after a transient failure (429, 5xx, timeout). Off by default because the API has no idempotency key — a timed-out POST may have already reached the exchange, and retrying it can place a duplicate order. |
52
52
  | `DHAN_AUTO_CORRELATION_ID=true` | `config.auto_correlation_id` | `false` | Fills in a `correlationId` (`dhq-<hex>`) on order placements that lack one, so a timed-out placement can be reconciled via `GET /v2/orders/external/{correlation-id}`. Off by default because it changes the request body; an explicit correlation id is always preserved. |
53
53
  | `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false` | `config.warn_on_ambiguous_write_failure` | `true` (on) | Logs a once-per-call-site deprecation notice when a non-bang write method (`Order.place`, `#modify`, …) reports failure as `nil`, `false`, or a `DhanHQ::ErrorObject` — these disagree today and unify on `ErrorObject` in 4.0.0. Use the `!` variant (`place!`, `#modify!`) to get a raised `DhanHQ::OrderError` instead of the ambiguous return value. |
54
+ | `DHAN_WS_DEBUG=true` | `config.ws_debug` | `false` | Logs every raw inbound WebSocket frame (market feed, order updates, market depth) as a hex dump at `debug` level before it's parsed. Off by default — high volume, and there's no encoding cost when disabled since the flag is checked first. Requires `DHAN_LOG_LEVEL=debug` (see [Logging](#logging)) to actually see the output. See [Troubleshooting](TROUBLESHOOTING.md#websocket-frame-debugging). |
54
55
 
55
56
  ## `.env` File Setup
56
57
 
@@ -21,6 +21,11 @@ bundle install
21
21
  If you package the gem privately you can also point to a released version from
22
22
  RubyGems.
23
23
 
24
+ **Fast path:** `rails generate dhanhq:install` scaffolds the initializer below plus
25
+ a sample order service, a Sidekiq market-feed worker, and an ActionCable channel
26
+ in one command. The rest of this guide covers what it generates and the options
27
+ beyond it (dynamic tokens, order-update streaming, scheduled jobs).
28
+
24
29
  ## 2. Configure credentials & initializer
25
30
 
26
31
  Store the Dhan client id and access token using Rails credentials or ENV
@@ -173,6 +178,32 @@ The gem exposes models for positions, holdings, trades, funds, option chains,
173
178
  historical bars, etc. Instantiate them the same way (`Model.all`, `.find`,
174
179
  `.where`, `#save`).
175
180
 
181
+ ### Placing orders in the background
182
+
183
+ For order placement from a controller action or another job, `DhanHQ::Jobs::PlaceOrderJob`
184
+ ships with the gem — no service object required:
185
+
186
+ ```ruby
187
+ DhanHQ::Jobs::PlaceOrderJob.perform_later(
188
+ transaction_type: DhanHQ::Constants::TransactionType::BUY,
189
+ exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
190
+ product_type: DhanHQ::Constants::ProductType::INTRADAY,
191
+ order_type: DhanHQ::Constants::OrderType::LIMIT,
192
+ validity: DhanHQ::Constants::Validity::DAY,
193
+ security_id: "11536",
194
+ quantity: 5,
195
+ price: 1500.0
196
+ )
197
+ ```
198
+
199
+ It calls `Order.place!` and uses ActiveJob's `discard_on` for `DhanHQ::OrderError` and
200
+ `DhanHQ::RiskViolation` — a rejection is logged, not silently retried. This matters because
201
+ DhanHQ order writes are **not idempotent**: most queue adapters (Sidekiq chief among them)
202
+ retry unhandled exceptions at the backend level by default, independent of ActiveJob's own
203
+ opt-in `retry_on`, and a timed-out `POST /v2/orders` retry can place a duplicate order. Don't
204
+ add your own `retry_on DhanHQ::OrderError` on top of this job — that would fight the
205
+ `discard_on` and reintroduce the exact risk it exists to prevent.
206
+
176
207
  ## 4. Centralise error handling
177
208
 
178
209
  Wrap the gem's exceptions in a concern so Rails controllers and jobs return
@@ -209,21 +240,26 @@ ticks through ActionCable, Redis, or a database.
209
240
  # app/workers/dhan/market_feed_worker.rb
210
241
  class Dhan::MarketFeedWorker
211
242
  include Sidekiq::Worker
243
+ sidekiq_options retry: false # DhanHQ::WS::Client already reconnects and re-subscribes on its own
212
244
 
213
- def perform(mode = :quote, securities = [])
214
- client = DhanHQ::WS::Client.new(mode: mode.to_sym)
215
-
216
- client.on(:open) { Rails.logger.info('Dhan WS connected') }
217
- client.on(:close) { Rails.logger.warn('Dhan WS closed; worker will retry') }
218
- client.on(:error) { |err| Rails.logger.error("Dhan WS error: #{err}") }
219
-
220
- client.on(:tick) do |tick|
245
+ def perform(mode = "quote", securities = [])
246
+ client = DhanHQ::WS.connect(mode: mode.to_sym) do |tick|
221
247
  ActionCable.server.broadcast('market_feed', tick)
222
248
  end
223
249
 
224
- client.start
225
- client.subscribe(securities) if securities.any?
226
- client.wait! # blocks the worker thread while EventMachine runs
250
+ client.on(:reconnect) { |info| Rails.logger.warn("Dhan WS reconnect ##{info[:attempt]}") }
251
+ client.on(:error) { |message| Rails.logger.error("Dhan WS error: #{message}") }
252
+
253
+ securities.each { |segment, security_id| client.subscribe_one(segment: segment, security_id: security_id) }
254
+
255
+ # Client#start already spawned a background thread and returned -- there is
256
+ # no blocking wait! method. Hold the worker open for the life of the
257
+ # connection so Sidekiq doesn't consider the job "done" the instant the
258
+ # feed opens.
259
+ loop do
260
+ sleep 30
261
+ break unless client.connected?
262
+ end
227
263
  end
228
264
  end
229
265
  ```
@@ -255,22 +291,29 @@ Use the order-update WebSocket endpoint (configure `ws_order_url` and
255
291
  # app/workers/dhan/order_updates_worker.rb
256
292
  class Dhan::OrderUpdatesWorker
257
293
  include Sidekiq::Worker
294
+ sidekiq_options retry: false
258
295
 
259
296
  def perform
260
- client = DhanHQ::WS::Client.new(kind: :order_updates)
261
-
262
- client.on(:order_update) do |payload|
263
- OrderStatusUpdater.call(payload)
297
+ client = DhanHQ::WS::Orders.connect do |order_update|
298
+ OrderStatusUpdater.call(order_update)
264
299
  end
265
300
 
266
301
  client.on(:error) { |err| Rails.logger.error("Dhan order WS error: #{err}") }
267
302
 
268
- client.start
269
- client.wait!
303
+ loop do
304
+ sleep 30
305
+ break unless client.connected?
306
+ end
270
307
  end
271
308
  end
272
309
  ```
273
310
 
311
+ `DhanHQ::WS::Orders::Client` also tracks state for you without any extra
312
+ wiring — `client.order_state(order_no)`, `client.orders_by_status(status)`,
313
+ `client.pending_orders`, and friends stay in sync as updates arrive, so
314
+ `OrderStatusUpdater` doesn't need to re-derive status transitions from raw
315
+ payloads.
316
+
274
317
  Inside `OrderStatusUpdater` you can reconcile the payload with your local order
275
318
  records, notify users via ActionCable or email, etc.
276
319
 
@@ -50,16 +50,30 @@ client = DhanHQ::WS.connect(mode: :ticker) { |tick| puts tick[:ltp] }
50
50
 
51
51
  **Solution:**
52
52
  - The client safely drops malformed frames and keeps the event loop alive.
53
- - Run with `DHAN_LOG_LEVEL=DEBUG` to inspect raw frames:
53
+ - Turn on [WebSocket Frame Debugging](#websocket-frame-debugging) below to see the actual bytes that failed to parse.
54
+
55
+ ---
56
+
57
+ ## WebSocket Frame Debugging
58
+
59
+ **Symptom:** Ticks stopped arriving, or you're seeing "unknown feed kind" / parse errors and need to see what the server actually sent — a bug report of "got garbage data" is unactionable without the raw bytes.
60
+
61
+ **Solution:** Set `DHAN_WS_DEBUG=true` (or `config.ws_debug = true`) alongside debug-level logging. Every raw inbound frame — market feed, order updates, market depth — is logged as a hex dump before it's parsed:
54
62
 
55
63
  ```bash
64
+ export DHAN_WS_DEBUG=true
56
65
  export DHAN_LOG_LEVEL=DEBUG
57
66
  ```
58
67
 
59
68
  ```ruby
69
+ DhanHQ.configure do |c|
70
+ c.ws_debug = true
71
+ end
60
72
  DhanHQ.logger.level = Logger::DEBUG
61
73
  ```
62
74
 
75
+ This is off by default and high-volume when on — the flag is checked before any hex encoding work, so leaving it off costs nothing, but a live feed with `DHAN_WS_DEBUG=true` will log every single tick. Turn it off once you have what you need. See [Configuration Reference](CONFIGURATION.md#behavior-flags) for the full flag reference.
76
+
63
77
  ---
64
78
 
65
79
  ## Authentication Errors
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Backtest
5
+ # Aggregated outcome of a Runner run: the trade log, a per-bar equity
6
+ # curve, and derived summary statistics. Pure math over data Runner
7
+ # already computed — no I/O.
8
+ class Result
9
+ attr_reader :trades, :equity_curve, :initial_capital
10
+
11
+ # @param trades [Array<DhanHQ::Backtest::Trade>]
12
+ # @param equity_curve [Array<Float>] mark-to-market equity, one point per candle
13
+ # @param initial_capital [Float]
14
+ def initialize(trades:, equity_curve:, initial_capital:)
15
+ @trades = trades
16
+ @equity_curve = equity_curve
17
+ @initial_capital = initial_capital.to_f
18
+ end
19
+
20
+ # @return [Float] equity after the last candle (or initial_capital if no candles ran)
21
+ def final_equity
22
+ equity_curve.last || initial_capital
23
+ end
24
+
25
+ # @return [Float] total return over the run, as a percentage of initial capital
26
+ def total_return_pct
27
+ return 0.0 if initial_capital.zero?
28
+
29
+ ((final_equity - initial_capital) / initial_capital) * 100
30
+ end
31
+
32
+ # @return [Integer] number of completed round-trip trades
33
+ def num_trades
34
+ trades.size
35
+ end
36
+
37
+ # @return [Array<DhanHQ::Backtest::Trade>] trades that closed profitably
38
+ def winning_trades
39
+ trades.select(&:win?)
40
+ end
41
+
42
+ # @return [Float] percentage of trades that closed profitably
43
+ def win_rate
44
+ return 0.0 if trades.empty?
45
+
46
+ (winning_trades.size.to_f / trades.size) * 100
47
+ end
48
+
49
+ # @return [Float] mean pnl across all trades
50
+ def avg_trade_pnl
51
+ return 0.0 if trades.empty?
52
+
53
+ trades.sum(&:pnl) / trades.size
54
+ end
55
+
56
+ # @return [Float] largest peak-to-trough decline in the equity curve, as a percentage
57
+ def max_drawdown_pct
58
+ return 0.0 if equity_curve.empty?
59
+
60
+ peak = equity_curve.first
61
+ max_dd = 0.0
62
+
63
+ equity_curve.each do |equity|
64
+ peak = equity if equity > peak
65
+ next if peak.zero?
66
+
67
+ drawdown = ((peak - equity) / peak) * 100
68
+ max_dd = drawdown if drawdown > max_dd
69
+ end
70
+
71
+ max_dd
72
+ end
73
+
74
+ # @return [Hash] rounded snapshot of the metrics above, for reporting
75
+ def summary
76
+ {
77
+ total_return_pct: total_return_pct.round(2),
78
+ num_trades: num_trades,
79
+ win_rate: win_rate.round(2),
80
+ avg_trade_pnl: avg_trade_pnl.round(2),
81
+ max_drawdown_pct: max_drawdown_pct.round(2),
82
+ final_equity: final_equity.round(2)
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Backtest
5
+ # Replays a DhanHQ::Strategy::Base against historical OHLC candles and
6
+ # returns a Result.
7
+ #
8
+ # Both entries and exits fill at the *next* candle's open, never the
9
+ # signal candle's own price — evaluating a signal on a candle and then
10
+ # filling somewhere inside that same candle is look-ahead bias, since the
11
+ # fill price would have to come from before the candle closed (and so
12
+ # before the signal could have fired live). The one exception is the end
13
+ # of the dataset: an open position with no next candle to fill against is
14
+ # force-closed at the last candle's close.
15
+ #
16
+ # Only one position is held at a time, matching DhanHQ::Strategy::Base's
17
+ # single `@position` / entry-then-exit model — no pyramiding, no shorting.
18
+ class Runner
19
+ DEFAULT_QUANTITY = ->(equity, price) { price.positive? ? (equity / price).floor : 0 }
20
+ NO_FEES = ->(_trade_value) { 0.0 }
21
+
22
+ # @param strategy [DhanHQ::Strategy::Base]
23
+ # @param data [DhanHQ::MarketData::OHLCSeries]
24
+ # @param initial_capital [Float]
25
+ # @param quantity [#call, Integer] `->(equity, price) { ... }`, or a fixed integer
26
+ # @param fees [#call] `->(trade_value) { ... }`, charged once per leg (entry, exit)
27
+ # @param max_bars_held [Integer, nil] force-exit a position held this many bars or longer
28
+ def initialize(strategy:, data:, initial_capital:, quantity: DEFAULT_QUANTITY, fees: NO_FEES, max_bars_held: nil)
29
+ @strategy = strategy
30
+ @candles = data.respond_to?(:candles) ? data.candles : Array(data)
31
+ @initial_capital = initial_capital.to_f
32
+ @quantity = quantity.respond_to?(:call) ? quantity : ->(_equity, _price) { quantity }
33
+ @fees = fees
34
+ @max_bars_held = max_bars_held
35
+ end
36
+
37
+ # @return [DhanHQ::Backtest::Result]
38
+ def run
39
+ return Result.new(trades: [], equity_curve: [], initial_capital: @initial_capital) if @candles.empty?
40
+
41
+ equity = @initial_capital
42
+ equity_curve = []
43
+ trades = []
44
+ window_candles = []
45
+ open_trade = nil
46
+ entry_index = nil
47
+
48
+ @candles.each_with_index do |candle, index|
49
+ window_candles << candle
50
+ window = DhanHQ::MarketData::OHLCSeries.new(window_candles)
51
+ has_next = index < @candles.size - 1
52
+
53
+ if open_trade && has_next
54
+ trade = maybe_exit(open_trade, window, index, entry_index, equity, equity_curve, @candles[index + 1])
55
+
56
+ if trade
57
+ trades << trade
58
+ equity += trade.pnl
59
+ open_trade = nil
60
+ entry_index = nil
61
+ end
62
+ elsif open_trade.nil? && has_next
63
+ open_trade, entry_index = maybe_enter(window, @candles[index + 1], equity, index)
64
+ end
65
+
66
+ # A position opened this very iteration (via maybe_enter) isn't filled until the
67
+ # *next* candle — mark-to-market must stay flat until index reaches entry_index,
68
+ # or the equity curve would price the position off a fill that hasn't happened yet.
69
+ filled = open_trade && index >= entry_index
70
+ equity_curve << mark_to_market(equity, filled ? open_trade : nil, candle.close)
71
+ end
72
+
73
+ if open_trade
74
+ last = @candles.last
75
+ trade = close_trade(open_trade, last.timestamp, last.close, :end_of_data)
76
+ trades << trade
77
+ equity += trade.pnl
78
+ equity_curve[-1] = equity
79
+ end
80
+
81
+ Result.new(trades: trades, equity_curve: equity_curve, initial_capital: @initial_capital)
82
+ end
83
+
84
+ private
85
+
86
+ def maybe_enter(window, next_candle, equity, index)
87
+ signal = @strategy.evaluate_entry(window)
88
+ return [nil, nil] unless signal.buy?
89
+
90
+ qty = @quantity.call(equity, next_candle.open)
91
+ return [nil, nil] unless qty.positive?
92
+
93
+ [{ entry_time: next_candle.timestamp, entry_price: next_candle.open, quantity: qty }, index + 1]
94
+ end
95
+
96
+ def maybe_exit(open_trade, window, index, entry_index, equity, equity_curve, next_candle)
97
+ bars_held = index - entry_index
98
+ forced = @max_bars_held && bars_held >= @max_bars_held
99
+ drawdown = drawdown_pct(equity_curve, equity)
100
+ violations = @strategy.check_risks(equity: equity, position: open_trade, drawdown: drawdown)
101
+ signal = @strategy.evaluate_exit(window)
102
+
103
+ return unless forced || violations.any? || signal.sell?
104
+
105
+ reason = if forced
106
+ :max_bars_held
107
+ elsif violations.any?
108
+ :risk_violation
109
+ else
110
+ :signal
111
+ end
112
+ close_trade(open_trade, next_candle.timestamp, next_candle.open, reason)
113
+ end
114
+
115
+ def close_trade(open_trade, exit_time, exit_price, reason)
116
+ entry_value = open_trade[:entry_price] * open_trade[:quantity]
117
+ exit_value = exit_price * open_trade[:quantity]
118
+
119
+ Trade.new(
120
+ entry_time: open_trade[:entry_time],
121
+ entry_price: open_trade[:entry_price],
122
+ exit_time: exit_time,
123
+ exit_price: exit_price,
124
+ quantity: open_trade[:quantity],
125
+ exit_reason: reason,
126
+ fees: @fees.call(entry_value) + @fees.call(exit_value)
127
+ )
128
+ end
129
+
130
+ def mark_to_market(equity, open_trade, close_price)
131
+ return equity unless open_trade
132
+
133
+ equity + ((close_price - open_trade[:entry_price]) * open_trade[:quantity])
134
+ end
135
+
136
+ def drawdown_pct(equity_curve, current_equity)
137
+ peak = (equity_curve + [current_equity]).max
138
+ return 0.0 if peak.nil? || peak.zero?
139
+
140
+ [((peak - current_equity) / peak) * 100, 0.0].max
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Backtest
5
+ # A single completed long round-trip trade produced by Runner.
6
+ #
7
+ # `fees` is the total cost charged across both legs (entry + exit), already
8
+ # netted into `pnl` — it is not deducted again by callers.
9
+ Trade = Struct.new(
10
+ :entry_time, :entry_price, :exit_time, :exit_price, :quantity, :exit_reason, :fees
11
+ ) do
12
+ # Net profit/loss for this trade, after fees.
13
+ #
14
+ # @return [Float]
15
+ def pnl
16
+ ((exit_price - entry_price) * quantity) - fees.to_f
17
+ end
18
+
19
+ # Net profit/loss as a percentage of the entry value.
20
+ #
21
+ # @return [Float]
22
+ def pnl_pct
23
+ entry_value = entry_price.to_f * quantity.to_f
24
+ return 0.0 if entry_value.zero?
25
+
26
+ (pnl / entry_value) * 100
27
+ end
28
+
29
+ # @return [Boolean] whether this trade closed profitably after fees
30
+ def win?
31
+ pnl.positive?
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ # Backtesting engine that replays a DhanHQ::Strategy::Base against historical
5
+ # OHLC data and reports trades, an equity curve, and summary performance stats.
6
+ #
7
+ # @example Backtest a strategy against daily candles
8
+ # data = DhanHQ::Models::HistoricalData.daily(
9
+ # security_id: "1333", exchange_segment: "NSE_EQ",
10
+ # instrument: "EQUITY", from_date: "2024-01-01", to_date: "2024-12-31"
11
+ # )
12
+ # series = DhanHQ::MarketData::OHLCSeries.from_response(data)
13
+ #
14
+ # result = DhanHQ::Backtest::Runner.new(
15
+ # strategy: MyStrategy.new,
16
+ # data: series,
17
+ # initial_capital: 100_000.0
18
+ # ).run
19
+ #
20
+ # result.summary #=> { total_return_pct: 12.4, num_trades: 8, win_rate: 62.5, ... }
21
+ #
22
+ module Backtest
23
+ end
24
+ end
@@ -132,6 +132,15 @@ module DhanHQ
132
132
  # @return [Integer]
133
133
  attr_accessor :market_depth_level
134
134
 
135
+ # When true, every raw inbound WebSocket frame (market feed, order updates,
136
+ # market depth) is logged as a hex dump at debug level before it's parsed.
137
+ # Off by default -- high volume, and the check happens before any hex
138
+ # encoding work so there's no cost when disabled.
139
+ #
140
+ # Set via +DHAN_WS_DEBUG=true+ or in {DhanHQ.configure}.
141
+ # @return [Boolean]
142
+ attr_accessor :ws_debug
143
+
135
144
  # Setters for websocket URLs
136
145
  attr_writer :ws_order_url, :ws_market_feed_url, :ws_market_depth_url
137
146
 
@@ -198,6 +207,11 @@ module DhanHQ
198
207
  @auto_correlation_id == true
199
208
  end
200
209
 
210
+ # @return [Boolean] True when raw WebSocket frames should be hex-logged.
211
+ def ws_debug?
212
+ @ws_debug == true
213
+ end
214
+
201
215
  # Initializes a new configuration instance with default values.
202
216
  #
203
217
  # @example
@@ -218,6 +232,7 @@ module DhanHQ
218
232
  @ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
219
233
  @ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
220
234
  @market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
235
+ @ws_debug = env_flag("DHAN_WS_DEBUG", default: false)
221
236
  @ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
222
237
  @partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
223
238
  @partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", nil)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Jobs
5
+ # Places a Dhan order via ActiveJob, without ever letting a queue adapter
6
+ # (Sidekiq, Resque, ...) retry it.
7
+ #
8
+ # DhanHQ order writes are not idempotent -- a timed-out POST /v2/orders may
9
+ # already have reached the exchange, so retrying it can place a duplicate
10
+ # order (see the README's "Order Retries and Duplicate Protection"). Most
11
+ # adapters retry unhandled exceptions at the backend level by default
12
+ # (Sidekiq's own retry, independent of ActiveJob's opt-in `retry_on`), so
13
+ # the only adapter-agnostic way to guarantee this write is never silently
14
+ # retried is to make sure the exception never reaches the adapter at all.
15
+ # `discard_on` does exactly that: it's handled inside ActiveJob's own
16
+ # `execute`, so from the adapter's point of view the job completed, not
17
+ # failed -- there is nothing left for the adapter's own retry logic to
18
+ # act on.
19
+ #
20
+ # @example
21
+ # DhanHQ::Jobs::PlaceOrderJob.perform_later(
22
+ # transaction_type: DhanHQ::Constants::TransactionType::BUY,
23
+ # exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
24
+ # product_type: DhanHQ::Constants::ProductType::INTRADAY,
25
+ # order_type: DhanHQ::Constants::OrderType::LIMIT,
26
+ # validity: DhanHQ::Constants::Validity::DAY,
27
+ # security_id: "11536",
28
+ # quantity: 5,
29
+ # price: 1500.0
30
+ # )
31
+ class PlaceOrderJob < ActiveJob::Base
32
+ discard_on DhanHQ::OrderError do |_job, error|
33
+ DhanHQ.logger&.error("[DhanHQ::Jobs::PlaceOrderJob] order rejected: #{error.message}")
34
+ end
35
+
36
+ discard_on DhanHQ::RiskViolation do |_job, error|
37
+ DhanHQ.logger&.warn("[DhanHQ::Jobs::PlaceOrderJob] risk check blocked the order: #{error.message}")
38
+ end
39
+
40
+ # @param params [Hash] Same params accepted by DhanHQ::Models::Order.place.
41
+ # @return [DhanHQ::Models::Order]
42
+ def perform(params)
43
+ DhanHQ::Models::Order.place!(params)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.3.0"
5
+ VERSION = "3.4.0"
6
6
  end
@@ -212,6 +212,7 @@ module DhanHQ
212
212
  # Handle WebSocket message event
213
213
  # @param ev [Event] WebSocket message event
214
214
  def handle_message(ev)
215
+ WS.debug_frame(self.class.name, ev.data)
215
216
  emit(:raw, ev.data)
216
217
  process_message(ev.data) if respond_to?(:process_message, true)
217
218
  end
@@ -141,6 +141,7 @@ module DhanHQ
141
141
  @ws.on(:open) { |_| handle_open(sessions) }
142
142
 
143
143
  @ws.on :message do |ev|
144
+ WS.debug_frame(self.class.name, ev.data)
144
145
  notify(:message, nil)
145
146
  @on_binary&.call(ev.data) # raw frames to decoder
146
147
  end
@@ -63,6 +63,7 @@ module DhanHQ
63
63
  # Process incoming WebSocket message
64
64
  # @param ev [Event] WebSocket message event
65
65
  def handle_message(ev)
66
+ WS.debug_frame(self.class.name, ev.data)
66
67
  msg = JSON.parse(ev.data, symbolize_names: true)
67
68
  emit(:raw, msg)
68
69
  emit(:message, msg)
data/lib/DhanHQ/ws.rb CHANGED
@@ -34,5 +34,30 @@ module DhanHQ
34
34
  def self.disconnect_all_local!
35
35
  Registry.stop_all
36
36
  end
37
+
38
+ # Cap on how many bytes of a frame get hex-dumped by {debug_frame}. A full
39
+ # depth packet can run several KB; at tick frequency that floods the log
40
+ # long before it adds diagnostic value beyond the first couple hundred
41
+ # bytes (header + the first few fields is normally enough to spot a
42
+ # parsing bug). The full frame size is always logged regardless of the cap.
43
+ DEBUG_FRAME_MAX_BYTES = 256
44
+
45
+ # Logs a raw inbound WebSocket frame as a hex dump when
46
+ # +config.ws_debug+ (+DHAN_WS_DEBUG=true+) is enabled. A no-op otherwise --
47
+ # the flag is checked before any hex encoding work, so there's no cost
48
+ # when debug logging is off. Frames longer than {DEBUG_FRAME_MAX_BYTES}
49
+ # are truncated in the dump, but the logged byte count is always the full
50
+ # frame size.
51
+ #
52
+ # @param source [String] short tag identifying which connection the frame came from
53
+ # @param data [String] raw frame bytes
54
+ # @return [void]
55
+ def self.debug_frame(source, data)
56
+ return unless DhanHQ.configuration&.ws_debug?
57
+
58
+ hex = data.byteslice(0, DEBUG_FRAME_MAX_BYTES).unpack1("H*")
59
+ hex += "...truncated" if data.bytesize > DEBUG_FRAME_MAX_BYTES
60
+ DhanHQ.logger&.debug("[DhanHQ::WS::#{source}] frame (#{data.bytesize} bytes): #{hex}")
61
+ end
37
62
  end
38
63
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Dhanhq
6
+ # Scaffolds a DhanHQ initializer, a sample order-placing service object, and
7
+ # a Sidekiq worker + ActionCable channel for streaming market data.
8
+ #
9
+ # rails generate dhanhq:install
10
+ #
11
+ class InstallGenerator < Rails::Generators::Base
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def create_initializer
15
+ template "initializer.rb", "config/initializers/dhanhq.rb"
16
+ end
17
+
18
+ def create_order_service
19
+ template "place_order_service.rb", "app/services/dhan/orders/place_order.rb"
20
+ end
21
+
22
+ def create_market_feed_worker
23
+ template "market_feed_worker.rb", "app/workers/dhan_market_feed_worker.rb"
24
+ end
25
+
26
+ def create_market_feed_channel
27
+ template "market_feed_channel.rb", "app/channels/dhan_market_feed_channel.rb"
28
+ end
29
+
30
+ def show_post_install_message
31
+ say ""
32
+ say "DhanHQ installed! Next steps:", :green
33
+ say " 1. Add your credentials:"
34
+ say " rails credentials:edit"
35
+ say " dhanhq:"
36
+ say " client_id: \"your_client_id\""
37
+ say " access_token: \"your_access_token\""
38
+ say " 2. Set LIVE_TRADING=true before placing real orders (see docs/CONFIGURATION.md)"
39
+ say " 3. Start the market feed: DhanMarketFeedWorker.perform_async"
40
+ say ""
41
+ say "Full reference: https://github.com/shubhamtaywade82/dhanhq-client/blob/main/docs/RAILS_INTEGRATION.md"
42
+ say ""
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dhan_hq"
4
+
5
+ if (creds = Rails.application.credentials.dig(:dhanhq))
6
+ ENV["DHAN_CLIENT_ID"] ||= creds[:client_id]
7
+ ENV["DHAN_ACCESS_TOKEN"] ||= creds[:access_token]
8
+ end
9
+
10
+ DhanHQ.configure_with_env
11
+
12
+ log_level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase
13
+ DhanHQ.logger.level = Logger.const_get(log_level)
14
+
15
+ # Full optional configuration (base_url, ws_order_url, partner auth, timeouts,
16
+ # DHAN_WS_DEBUG, ...) is documented in:
17
+ # https://github.com/shubhamtaywade82/dhanhq-client/blob/main/docs/CONFIGURATION.md
18
+ # https://github.com/shubhamtaywade82/dhanhq-client/blob/main/docs/RAILS_INTEGRATION.md
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DhanMarketFeedChannel < ApplicationCable::Channel
4
+ def subscribed
5
+ stream_from "dhan_market_feed"
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Keeps a DhanHQ market feed connection open and broadcasts ticks over
4
+ # ActionCable. Start it with DhanMarketFeedWorker.perform_async -- the job
5
+ # blocks for the life of the connection rather than completing immediately,
6
+ # so Sidekiq's dashboard reflects whether the feed is actually running.
7
+ #
8
+ # retry: false because there is nothing to retry: the underlying
9
+ # DhanHQ::WS::Client already reconnects and re-subscribes on its own.
10
+ class DhanMarketFeedWorker
11
+ include Sidekiq::Worker
12
+ sidekiq_options retry: false
13
+
14
+ def perform(mode = "quote")
15
+ client = DhanHQ::WS.connect(mode: mode.to_sym) do |tick|
16
+ ActionCable.server.broadcast("dhan_market_feed", tick)
17
+ end
18
+
19
+ client.on(:reconnect) { |info| Rails.logger.warn("[DhanMarketFeedWorker] reconnect ##{info[:attempt]}") }
20
+ client.on(:error) { |message| Rails.logger.error("[DhanMarketFeedWorker] #{message}") }
21
+
22
+ loop do
23
+ sleep 30
24
+ break unless client.connected?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dhan
4
+ module Orders
5
+ # Places a Dhan order via the bang variant, so a rejection raises a
6
+ # specific, catchable error instead of Order.place's ambiguous
7
+ # nil/false/ErrorObject return.
8
+ class PlaceOrder
9
+ def initialize(params)
10
+ @params = params
11
+ end
12
+
13
+ def call
14
+ DhanHQ::Models::Order.place!(@params)
15
+ rescue DhanHQ::OrderError => e
16
+ Rails.logger.error("Dhan order rejected: #{e.message}")
17
+ raise
18
+ rescue DhanHQ::RiskViolation => e
19
+ Rails.logger.warn("Dhan risk check blocked the order: #{e.message}")
20
+ raise
21
+ end
22
+ end
23
+ end
24
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-26 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: dry-validation
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ">="
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '1.11'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ">="
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0'
96
+ version: '1.11'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: eventmachine
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -164,10 +164,11 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description: A production-grade Ruby SDK for Dhan API v2 built for algo trading, portfolio
168
- monitoring, and live trading systems. Provides typed models, token lifecycle management,
169
- dry-validation contracts, resilient WebSocket streaming, and safety-focused order
170
- workflows for Ruby applications.
167
+ description: A production-grade Ruby SDK and Ruby client for Dhan API v2 built for
168
+ algorithmic trading, portfolio monitoring, and live trading systems on NSE, BSE
169
+ and MCX. Provides typed models, token lifecycle management, dry-validation contracts,
170
+ resilient WebSocket streaming with auto-reconnect, and safety-focused order workflows
171
+ for Ruby on Rails and standalone Ruby applications.
171
172
  email:
172
173
  - shubhamtaywade82@gmail.com
173
174
  executables:
@@ -228,6 +229,10 @@ files:
228
229
  - lib/DhanHQ/auth/token_generator.rb
229
230
  - lib/DhanHQ/auth/token_manager.rb
230
231
  - lib/DhanHQ/auth/token_renewal.rb
232
+ - lib/DhanHQ/backtest.rb
233
+ - lib/DhanHQ/backtest/result.rb
234
+ - lib/DhanHQ/backtest/runner.rb
235
+ - lib/DhanHQ/backtest/trade.rb
231
236
  - lib/DhanHQ/client.rb
232
237
  - lib/DhanHQ/concerns/bang_writes.rb
233
238
  - lib/DhanHQ/concerns/order_audit.rb
@@ -283,6 +288,7 @@ files:
283
288
  - lib/DhanHQ/helpers/response_helper.rb
284
289
  - lib/DhanHQ/helpers/validation_helper.rb
285
290
  - lib/DhanHQ/indicators.rb
291
+ - lib/DhanHQ/jobs/place_order_job.rb
286
292
  - lib/DhanHQ/json_loader.rb
287
293
  - lib/DhanHQ/market_data.rb
288
294
  - lib/DhanHQ/market_data/market_snapshot.rb
@@ -433,6 +439,11 @@ files:
433
439
  - lib/dhanhq/analysis/multi_timeframe_analyzer.rb
434
440
  - lib/dhanhq/analysis/options_buying_advisor.rb
435
441
  - lib/dhanhq/contracts/options_buying_advisor_contract.rb
442
+ - lib/generators/dhanhq/install/install_generator.rb
443
+ - lib/generators/dhanhq/install/templates/initializer.rb
444
+ - lib/generators/dhanhq/install/templates/market_feed_channel.rb
445
+ - lib/generators/dhanhq/install/templates/market_feed_worker.rb
446
+ - lib/generators/dhanhq/install/templates/place_order_service.rb
436
447
  - lib/rubocop/cop/dhanhq/use_constants.rb
437
448
  - lib/ta.rb
438
449
  - lib/ta/candles.rb
@@ -469,14 +480,15 @@ files:
469
480
  - skills/dhanhq-ruby/scripts/resolve_security.rb
470
481
  - skills/dhanhq-ruby/scripts/trade_logger.rb
471
482
  - skills/dhanhq-ruby/scripts/validate_order.rb
472
- homepage: https://github.com/shubhamtaywade82/dhanhq-client
483
+ homepage: https://shubhamtaywade82.github.io/dhanhq-client/
473
484
  licenses:
474
485
  - MIT
475
486
  metadata:
476
487
  allowed_push_host: https://rubygems.org
477
- homepage_uri: https://github.com/shubhamtaywade82/dhanhq-client
488
+ homepage_uri: https://shubhamtaywade82.github.io/dhanhq-client/
478
489
  source_code_uri: https://github.com/shubhamtaywade82/dhanhq-client
479
490
  changelog_uri: https://github.com/shubhamtaywade82/dhanhq-client/blob/main/CHANGELOG.md
491
+ documentation_uri: https://shubhamtaywade82.github.io/dhanhq-client/
480
492
  rubygems_mfa_required: 'true'
481
493
  post_install_message:
482
494
  rdoc_options: []
@@ -496,5 +508,6 @@ requirements: []
496
508
  rubygems_version: 3.5.11
497
509
  signing_key:
498
510
  specification_version: 4
499
- summary: The Ruby SDK for Dhan API v2 with REST, WebSocket, and trading workflows.
511
+ summary: Production-grade Ruby SDK for Dhan API v2 with REST APIs, WebSocket market
512
+ data, token lifecycle management, dry-validation contracts and trading workflows.
500
513
  test_files: []