DhanHQ 3.2.1 → 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/CHANGELOG.md +48 -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/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 +7 -0
- 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/version.rb +1 -1
- data/lib/DhanHQ/write_result.rb +163 -0
- data/lib/dhan_hq.rb +5 -0
- data/skills/dhanhq-ruby/references/portfolio.md +15 -8
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 065ac08985790dc4172816430025c482f539b506445c84181f69bdb56d30f510
|
|
4
|
+
data.tar.gz: 8f545f2ac8416dfadfe4fc0a5216b4203d06eaf05e1b1094d14ee571e84de342
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27d821b96f67997c64f1a523c2a366736dabcea5175e621014e22c9b691f35835f008f3bacaefe6b26229d2de92a78d44719f1b5ebff3e288960f156f7192331
|
|
7
|
+
data.tar.gz: 76558d36116bbc9c2b024aacc0c6cb1c30640b777cf2cd0832cb6d8da9b142fd6aca350318489a3d13f848136c1089bcd1ef014a2a975c8f66bde1e7c8131d10
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
## [3.3.0] - 2026-07-26
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Bang variants for every write method with a falsy failure contract** — `Order.place!`, `Order#modify!`/`#cancel!`/`#refresh!`, `SuperOrder.create!`/`#modify!`/`#cancel!`, `ForeverOrder.create!`, `IcebergOrder.create!`, `TwapOrder.create!`, `AlertOrder.create!`/`.modify!`, `PnlExit.configure!`/`.stop!`, `MultiOrder.place!`, `GlobalStocks::Order.place!`/`#modify!`/`#cancel!`.
|
|
6
|
+
|
|
7
|
+
Each raises `DhanHQ::OrderError` — which descends from `DhanHQ::Error`, so an existing `rescue DhanHQ::Error` handler still catches it — carrying whatever diagnostics the failure held. The non-bang methods are **untouched**: they return exactly what they returned before, so no existing caller changes behaviour.
|
|
8
|
+
|
|
9
|
+
This is step one of unifying the write return contracts. Today those contracts disagree: depending on the class and the failure, a rejected write comes back as `nil`, as `false`, or as a `DhanHQ::ErrorObject`, and `AlertOrder.modify` can return either of the first two from the same method. A caller cannot write one error branch. Unifying them outright would be a silent breaking change for dependent applications, because `nil` and `false` are falsy while `ErrorObject` is truthy — every `if result` failure branch in a dependent would quietly invert. So the migration is staged:
|
|
10
|
+
|
|
11
|
+
1. **This release** — additive bang variants. Opt in per call site.
|
|
12
|
+
2. **This release** — log a deprecation whenever a non-bang write returns a falsy failure, to find the remaining call sites from dependents' logs.
|
|
13
|
+
3. **4.0.0** — non-bang methods return `ErrorObject` uniformly, gated on step 2 going quiet.
|
|
14
|
+
|
|
15
|
+
- **Deprecation notices for ambiguous write failures** (step two). When a non-bang write returns `nil`, `false` or a `DhanHQ::ErrorObject`, the SDK now logs once per call site:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
[DhanHQ] DEPRECATION: DhanHQ::Models::Order.place reported failure as nil. Write
|
|
19
|
+
methods return nil, false or a DhanHQ::ErrorObject inconsistently today and will all
|
|
20
|
+
return DhanHQ::ErrorObject in 4.0.0, which is truthy — an `if result` failure branch
|
|
21
|
+
will invert. Use DhanHQ::Models::Order.place! to get a DhanHQ::OrderError instead...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The point is to surface the remaining call sites from dependent applications' logs before 4.0.0 changes any return value. Once per call site per process, not once per failure — a session that rejects hundreds of orders would otherwise produce noise that gets filtered out, defeating the purpose.
|
|
25
|
+
|
|
26
|
+
This layer only observes: it never alters a return value and never raises. It is silent on success, silent when reached through a bang variant (those callers have already migrated), and silent when you set `config.warn_on_ambiguous_write_failure = false` / `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false`. Defaults to on, because a notice nobody sees finds nothing.
|
|
27
|
+
|
|
28
|
+
- **`DhanHQ::Concerns::TrackedWrites`** — installs the observation. Uses `prepend` rather than `include`, since the write methods are defined directly on the model classes and an included module would sit behind them in the ancestor chain and never be reached.
|
|
29
|
+
- **`DhanHQ::Deprecation`** — once-per-key notice registry, thread-safe, with `warned_keys` and `reset!` for tests.
|
|
30
|
+
- **`DhanHQ::WriteResult`** — puts the knowledge of what a write failure looks like in one place (`failure?`, `success?`, `unwrap!`). `BaseModel#save!` now uses it instead of duplicating the same three-way check inline.
|
|
31
|
+
- **`DhanHQ::Concerns::BangWrites`** — generates the bang variants by delegating to their non-bang counterparts, so the two cannot drift: no duplicated request building, validation or logging. Generated as a module, so a hand-written `place!` can still override and call `super`.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`DhanHQ::MCP` was unreachable on a bare `require "dhan_hq"`.** Same failure as the `DhanHQ::AI` fix earlier in this release: `mcp.rb` defines `DhanHQ::MCP` while Zeitwerk's inflector expected `DhanHQ::Mcp` from the filename, so `DhanHQ::MCP::Server` raised `NameError` unless something had already loaded the file by hand — which only `exe/dhanhq-mcp` and `lib/dhan_hq/mcp.rb` did. Found the same way the `AI` bug was: cross-referencing every `DhanHQ::` constant named in the docs against what actually resolves. Guarded by a new `spec/dhan_hq/zeitwerk_autoload_spec.rb`, which shells out to a subprocess so the check can't be satisfied by another spec having already loaded the file by hand.
|
|
36
|
+
- **`lib/DhanHQ/configuration.rb`'s doc comment referenced `DhanHQ::Models::Order.find_by_correlation_id`, which does not exist** — the real method is `.find_by_correlation` (no `_id` suffix). Caught during the same audit.
|
|
37
|
+
- A documentation pass across the gem ahead of this release, cross-checking every code example and referenced class/method against the actual codebase:
|
|
38
|
+
- `docs/CONFIGURATION.md`'s resource table named `DhanHQ::Models::Fund` (real class: `Funds`) and `DhanHQ::Models::Ledger` (real class: `LedgerEntry`), and listed `fund_limit`/`margin_calculator` as `Funds` methods — neither exists; the real methods are `Funds.fetch`/`.balance` and `Margin.calculate`/`.calculate_multi`. The table now also covers the resources 3.2.0/3.3.0 added (Iceberg/TWAP/Alert orders, Multi Order, P&L Exit, eDIS, Global Stocks) and lists the `!` write variants.
|
|
39
|
+
- `docs/CONFIGURATION.md` was missing `LIVE_TRADING`, `DHAN_DRY_RUN`, `DHAN_RETRY_WRITES`, `DHAN_AUTO_CORRELATION_ID`, `DHAN_WARN_AMBIGUOUS_WRITE_FAILURE` and `DHAN_MARKET_DEPTH_LEVEL` entirely, and documented `DHAN_LOG_LEVEL` as if the library read it automatically — it doesn't; only the existing `## Logging` snippet wires it up.
|
|
40
|
+
- `skills/dhanhq-ruby/references/portfolio.md`'s eDIS section had the class name miscased (`EDIS` vs. `Edis`), called a nonexistent `.open_browser_for_tpin`, called `.inquiry` instead of `.inquire`, and accessed the (Hash) result via method calls instead of keys.
|
|
41
|
+
- `README.md` called `DhanHQ::Models::Fund.balance` — same class-name typo as above.
|
|
42
|
+
- `docs/RELEASE_GUIDE.md` claimed `Required Ruby: >= 3.1.0`; the gemspec has required `>= 3.2.0` since 3.0.0.
|
|
43
|
+
- `GUIDE.md` had no mention of the bang write variants added in this release; added a short section pointing to the README's fuller treatment.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `BaseModel#save!`'s exception message is now `"<Class>#save failed: <details>"` rather than `"Failed to save the record: <details>"`. The exception class is unchanged (`DhanHQ::Error`), and nothing in the gem, specs or docs asserted the old text.
|
|
48
|
+
|
|
1
49
|
## [3.2.1] - 2026-07-26
|
|
2
50
|
|
|
3
51
|
### Fixed
|
data/GUIDE.md
CHANGED
|
@@ -186,6 +186,22 @@ DhanHQ::Contracts::ModifyOrderContract.new.call(params).success?
|
|
|
186
186
|
DhanHQ::Models::Order.resource.update("123", params)
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### Explicit Failures: Bang Variants
|
|
190
|
+
|
|
191
|
+
`place`, `#modify`, `#cancel` and their equivalents across every write model do not agree on
|
|
192
|
+
how they report failure — depending on the class, a rejected write comes back as `nil`,
|
|
193
|
+
`false`, or a `DhanHQ::ErrorObject`. Each has a `!` variant that raises `DhanHQ::OrderError`
|
|
194
|
+
instead, so one `rescue` handles every failure the same way:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError on failure
|
|
198
|
+
order.cancel! # raises if the exchange did not cancel
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The non-bang methods are unchanged — adopting the bang variant is opt-in, per call site.
|
|
202
|
+
See the [README](README.md#explicit-failures-bang-variants) for the full list of `!` methods
|
|
203
|
+
and the deprecation notice that flags remaining non-bang call sites ahead of 4.0.0.
|
|
204
|
+
|
|
189
205
|
### Slicing Orders
|
|
190
206
|
|
|
191
207
|
Use the same fields as placement, but the contract allows additional validity options (`GTC`, `GTD`). The model helper accepts snake_case parameters and handles camelCase conversion as part of validation:
|
data/README.md
CHANGED
|
@@ -314,6 +314,59 @@ risk:
|
|
|
314
314
|
DhanHQ.configure { |config| config.retry_non_idempotent_writes = true }
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
+
### Explicit Failures: Bang Variants
|
|
318
|
+
|
|
319
|
+
Every write method has a `!` variant that raises instead of returning a falsy value:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
order = DhanHQ::Models::Order.place!(params) # raises DhanHQ::OrderError on failure
|
|
323
|
+
order.cancel! # raises if the exchange did not cancel
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
This exists because the non-bang methods do not agree on how to report failure — some
|
|
327
|
+
return `nil`, some `false`, some a `DhanHQ::ErrorObject` — so a caller cannot write one
|
|
328
|
+
error branch:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
# Before: which falsy thing came back depends on which method and which failure
|
|
332
|
+
order = DhanHQ::Models::Order.place(params)
|
|
333
|
+
return unless order # nil on rejection
|
|
334
|
+
|
|
335
|
+
result = DhanHQ::Models::MultiOrder.place(legs)
|
|
336
|
+
return if result.is_a?(DhanHQ::ErrorObject) # truthy! `unless result` would not catch this
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`DhanHQ::OrderError` descends from `DhanHQ::Error`, so an existing `rescue DhanHQ::Error`
|
|
340
|
+
keeps working. The non-bang methods are unchanged, so adopting this is per call site:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
begin
|
|
344
|
+
DhanHQ::Models::Order.place!(params)
|
|
345
|
+
rescue DhanHQ::OrderError => e
|
|
346
|
+
logger.error(e.message) # carries the API's diagnostics
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
When a non-bang write reports failure, the SDK logs once per call site to help you find
|
|
351
|
+
what needs migrating before 4.0.0 changes the return value:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
[DhanHQ] DEPRECATION: DhanHQ::Models::Order.place reported failure as nil. ...
|
|
355
|
+
Use DhanHQ::Models::Order.place! to get a DhanHQ::OrderError instead, or set
|
|
356
|
+
config.warn_on_ambiguous_write_failure = false to silence this.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
It fires once per call site per process, never alters a return value, never raises, and
|
|
360
|
+
goes quiet once you switch that site to the bang variant. To silence it entirely:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
DhanHQ.configure { |c| c.warn_on_ambiguous_write_failure = false } # or DHAN_WARN_AMBIGUOUS_WRITE_FAILURE=false
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Available on `Order`, `SuperOrder`, `ForeverOrder`, `IcebergOrder`, `TwapOrder`,
|
|
367
|
+
`AlertOrder`, `PnlExit`, `MultiOrder` and `GlobalStocks::Order`. The non-bang methods are
|
|
368
|
+
planned to converge on `ErrorObject` in 4.0.0 — see the CHANGELOG for the staged plan.
|
|
369
|
+
|
|
317
370
|
### Order Audit Logging
|
|
318
371
|
|
|
319
372
|
Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:
|
|
@@ -374,7 +427,7 @@ order.cancel
|
|
|
374
427
|
```ruby
|
|
375
428
|
DhanHQ::Models::Position.all
|
|
376
429
|
DhanHQ::Models::Holding.all
|
|
377
|
-
DhanHQ::Models::
|
|
430
|
+
DhanHQ::Models::Funds.balance
|
|
378
431
|
```
|
|
379
432
|
|
|
380
433
|
### Historical Data
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -24,7 +24,6 @@ Set these _before_ calling `configure_with_env` to override defaults:
|
|
|
24
24
|
|
|
25
25
|
| Variable | Default | Description |
|
|
26
26
|
| -------------------------------- | ---------- | ------------------------------------------------------- |
|
|
27
|
-
| `DHAN_LOG_LEVEL` | `INFO` | Logger verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`) |
|
|
28
27
|
| `DHAN_SANDBOX` | `false` | Set `"true"` to route REST calls to `https://sandbox.dhan.co/v2` instead of production. Note: Dhan's sandbox validates request/response plumbing only — it does not execute real order fills/matching. Placed orders stay `PENDING` indefinitely. See [ENDPOINTS_AND_SANDBOX.md](ENDPOINTS_AND_SANDBOX.md). |
|
|
29
28
|
| `DHAN_BASE_URL` | Dhan prod | Point REST calls to a different API hostname. Takes precedence over `DHAN_SANDBOX` only when explicitly set to something other than the production default. |
|
|
30
29
|
| `DHAN_WS_VERSION` | latest | Pin WebSocket connections to a specific API version |
|
|
@@ -32,12 +31,27 @@ Set these _before_ calling `configure_with_env` to override defaults:
|
|
|
32
31
|
| `DHAN_WS_USER_TYPE` | `SELF` | Switch between `SELF` and `PARTNER` streaming modes |
|
|
33
32
|
| `DHAN_PARTNER_ID` | — | Required when `DHAN_WS_USER_TYPE=PARTNER` |
|
|
34
33
|
| `DHAN_PARTNER_SECRET` | — | Required when `DHAN_WS_USER_TYPE=PARTNER` |
|
|
34
|
+
| `DHAN_MARKET_DEPTH_LEVEL` | `20` | WebSocket market depth levels to subscribe to |
|
|
35
35
|
| `DHAN_CONNECT_TIMEOUT` | `10` | Connection timeout in seconds |
|
|
36
36
|
| `DHAN_READ_TIMEOUT` | `30` | Read timeout in seconds |
|
|
37
37
|
| `DHAN_WRITE_TIMEOUT` | `30` | Write timeout in seconds |
|
|
38
38
|
| `DHAN_WS_MAX_TRACKED_ORDERS` | `10000` | Maximum orders to track in WebSocket |
|
|
39
39
|
| `DHAN_WS_MAX_ORDER_AGE` | `604800` | Maximum order age in seconds before cleanup (7 days) |
|
|
40
40
|
|
|
41
|
+
`DHAN_LOG_LEVEL` is **not** read automatically — see [Logging](#logging) below for the one-line snippet that wires it up.
|
|
42
|
+
|
|
43
|
+
## Behavior Flags
|
|
44
|
+
|
|
45
|
+
These change what a write request *does*, not just where it goes. All default to the safe/conservative behavior and are off unless set.
|
|
46
|
+
|
|
47
|
+
| Variable | Config attribute | Default | Effect |
|
|
48
|
+
| ------------------------------------- | ------------------------------------ | ------- | ------ |
|
|
49
|
+
| `LIVE_TRADING=true` | — | unset | **Required to place, modify, or cancel any real order, position exit, kill switch, EDIS, or P&L exit.** Without it, every write-path resource raises `DhanHQ::LiveTradingDisabledError` before making the request. This is the primary safety gate — see [ARCHITECTURE.md](../ARCHITECTURE.md) and the risk pipeline. |
|
|
50
|
+
| `DHAN_DRY_RUN=true` | `config.dry_run` | `false` | Suppresses every state-changing request, logs the payload as `DHAN_DRY_RUN`, and answers order placements with a simulated `DRYRUN-…` id so caller code paths still run to completion. Reads still hit the API. |
|
|
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
|
+
| `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
|
+
| `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
|
+
|
|
41
55
|
## `.env` File Setup
|
|
42
56
|
|
|
43
57
|
Create a `.env` file in your project root:
|
|
@@ -95,16 +109,26 @@ For detailed authentication flows, see [AUTHENTICATION.md](AUTHENTICATION.md).
|
|
|
95
109
|
|
|
96
110
|
## Available Resources
|
|
97
111
|
|
|
98
|
-
| Resource | Model
|
|
99
|
-
| ------------------------ |
|
|
100
|
-
| Orders | `DhanHQ::Models::Order`
|
|
101
|
-
| Trades | `DhanHQ::Models::Trade`
|
|
102
|
-
| Forever Orders | `DhanHQ::Models::ForeverOrder`
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
112
|
+
| Resource | Model | Actions |
|
|
113
|
+
| ------------------------ | ---------------------------------------- | ------------------------------------------------------------------------------ |
|
|
114
|
+
| Orders | `DhanHQ::Models::Order` | `place`, `find`, `all`, `where`, `#modify`, `#cancel`, `#destroy` (`!` variants raise) |
|
|
115
|
+
| Trades | `DhanHQ::Models::Trade` | `all`, `find_by_order_id` |
|
|
116
|
+
| Forever Orders | `DhanHQ::Models::ForeverOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
|
|
117
|
+
| Iceberg Orders | `DhanHQ::Models::IcebergOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
|
|
118
|
+
| TWAP Orders | `DhanHQ::Models::TwapOrder` | `create`, `find`, `all`, `#modify`, `#cancel` |
|
|
119
|
+
| Alert Orders | `DhanHQ::Models::AlertOrder` | `create`, `find`, `all`, `modify(alert_id, params)`, `#destroy` |
|
|
120
|
+
| Super Orders | `DhanHQ::Models::SuperOrder` | `create`, `all`, `#modify`, `#cancel(leg_name)` |
|
|
121
|
+
| Multi Order (basket) | `DhanHQ::Models::MultiOrder` | `place(orders, dhan_client_id:)` — up to 15 legs |
|
|
122
|
+
| P&L Exit | `DhanHQ::Models::PnlExit` | `configure`, `stop`, `status` |
|
|
123
|
+
| Holdings | `DhanHQ::Models::Holding` | `all` |
|
|
124
|
+
| Positions | `DhanHQ::Models::Position` | `all`, `active`, `#convert(params)`, `.exit_all!` |
|
|
125
|
+
| Funds | `DhanHQ::Models::Funds` | `fetch`, `balance` |
|
|
126
|
+
| Margin Calculator | `DhanHQ::Models::Margin` | `calculate(params)`, `calculate_multi(params)` |
|
|
127
|
+
| Ledger | `DhanHQ::Models::LedgerEntry` | `all(from_date:, to_date:)` |
|
|
128
|
+
| eDIS | `DhanHQ::Models::Edis` | `generate_tpin`, `generate_form`, `generate_bulk_form`, `inquire(isin:)` |
|
|
129
|
+
| Market Feeds | `DhanHQ::Models::MarketFeed` | `ltp`, `ohlc`, `quote` |
|
|
130
|
+
| Historical Data (Charts) | `DhanHQ::Models::HistoricalData` | `daily`, `intraday` |
|
|
131
|
+
| Option Chain | `DhanHQ::Models::OptionChain` | `fetch`, `fetch_expiry_list` |
|
|
132
|
+
| Global Stocks Orders | `DhanHQ::Models::GlobalStocks::Order` | `place`, `find`, `all`, `#cancel` — see [ARCHITECTURE.md](../ARCHITECTURE.md) |
|
|
133
|
+
|
|
134
|
+
`#method` denotes an instance method (called on a fetched or created record); everything else is a class method. Every write method above also has a `!` variant (`place!`, `#modify!`, `#cancel!`, …) that raises `DhanHQ::OrderError` instead of returning a falsy value or an `ErrorObject` — see the [CHANGELOG](../CHANGELOG.md) for the write-return-contract migration this is part of.
|
data/docs/RELEASE_GUIDE.md
CHANGED
|
@@ -434,7 +434,7 @@ git push origin main v2.1.12
|
|
|
434
434
|
|
|
435
435
|
- **Gem Name:** DhanHQ
|
|
436
436
|
- **Current Version:** Check `lib/DhanHQ/version.rb`
|
|
437
|
-
- **Required Ruby:** >= 3.
|
|
437
|
+
- **Required Ruby:** >= 3.2.0
|
|
438
438
|
- **License:** MIT
|
|
439
439
|
- **Homepage:** https://github.com/shubhamtaywade82/dhanhq-client
|
|
440
440
|
|
|
@@ -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"
|
|
@@ -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/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
|
@@ -35,6 +35,11 @@ module DhanHQ
|
|
|
35
35
|
"base_api" => "BaseAPI",
|
|
36
36
|
"ip_setup" => "IPSetup",
|
|
37
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",
|
|
38
43
|
"ws" => "WS"
|
|
39
44
|
)
|
|
40
45
|
LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
|
|
@@ -70,24 +70,31 @@ position.convert(
|
|
|
70
70
|
|
|
71
71
|
## eDIS Authorization
|
|
72
72
|
|
|
73
|
-
For selling delivery holdings, authorization is handled via `DhanHQ::Models::
|
|
73
|
+
For selling delivery holdings, authorization is handled via `DhanHQ::Models::Edis`:
|
|
74
74
|
|
|
75
75
|
### Step 1: Generate TPIN
|
|
76
76
|
```ruby
|
|
77
|
-
DhanHQ::Models::
|
|
77
|
+
DhanHQ::Models::Edis.generate_tpin
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Triggers a TPIN to the user's registered mobile/email. Returns `{status: "accepted"}` — the API responds `202` for this async operation.
|
|
81
|
+
|
|
82
|
+
### Step 2: Generate and Render the Authorization Form
|
|
81
83
|
```ruby
|
|
82
|
-
DhanHQ::Models::
|
|
84
|
+
form = DhanHQ::Models::Edis.generate_form(
|
|
83
85
|
isin: "INE002A01018",
|
|
84
86
|
qty: 5,
|
|
85
|
-
exchange: "NSE"
|
|
87
|
+
exchange: "NSE",
|
|
88
|
+
segment: "EQ"
|
|
86
89
|
)
|
|
90
|
+
# form[:edisFormHtml] is a browser-postable HTML form; render or POST it so the
|
|
91
|
+
# user can complete authorization on Dhan's eDIS page.
|
|
87
92
|
```
|
|
88
93
|
|
|
89
|
-
### Step 3:
|
|
94
|
+
### Step 3: Inquire eDIS Approval
|
|
90
95
|
```ruby
|
|
91
|
-
|
|
92
|
-
puts "Approved Qty: #{
|
|
96
|
+
status = DhanHQ::Models::Edis.inquire(isin: "INE002A01018") # or isin: "ALL"
|
|
97
|
+
puts "Approved Qty: #{status[:aprvdQty]}, Status: #{status[:status]}"
|
|
93
98
|
```
|
|
99
|
+
|
|
100
|
+
`inquire` returns the raw API response (a `HashWithIndifferentAccess`), not a model instance — key names match the API's camelCase (`aprvdQty`, `totalQty`), not the snake_case used elsewhere in this gem.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DhanHQ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
@@ -229,7 +229,9 @@ files:
|
|
|
229
229
|
- lib/DhanHQ/auth/token_manager.rb
|
|
230
230
|
- lib/DhanHQ/auth/token_renewal.rb
|
|
231
231
|
- lib/DhanHQ/client.rb
|
|
232
|
+
- lib/DhanHQ/concerns/bang_writes.rb
|
|
232
233
|
- lib/DhanHQ/concerns/order_audit.rb
|
|
234
|
+
- lib/DhanHQ/concerns/tracked_writes.rb
|
|
233
235
|
- lib/DhanHQ/configuration.rb
|
|
234
236
|
- lib/DhanHQ/constants.rb
|
|
235
237
|
- lib/DhanHQ/contracts/alert_order_contract.rb
|
|
@@ -266,6 +268,7 @@ files:
|
|
|
266
268
|
- lib/DhanHQ/core/base_model.rb
|
|
267
269
|
- lib/DhanHQ/core/base_resource.rb
|
|
268
270
|
- lib/DhanHQ/core/error_handler.rb
|
|
271
|
+
- lib/DhanHQ/deprecation.rb
|
|
269
272
|
- lib/DhanHQ/dry_run/ledger.rb
|
|
270
273
|
- lib/DhanHQ/dry_run/simulator.rb
|
|
271
274
|
- lib/DhanHQ/error_object.rb
|
|
@@ -391,6 +394,7 @@ files:
|
|
|
391
394
|
- lib/DhanHQ/utils/network_inspector.rb
|
|
392
395
|
- lib/DhanHQ/version.rb
|
|
393
396
|
- lib/DhanHQ/write_paths.rb
|
|
397
|
+
- lib/DhanHQ/write_result.rb
|
|
394
398
|
- lib/DhanHQ/ws.rb
|
|
395
399
|
- lib/DhanHQ/ws/base_connection.rb
|
|
396
400
|
- lib/DhanHQ/ws/client.rb
|