DhanHQ 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +51 -0
- data/README.md +152 -4
- data/docs/CONSTANTS_REFERENCE.md +3 -2
- data/exe/dhanhq-mcp +7 -0
- data/lib/DhanHQ/agent/tool_registry.rb +51 -2
- data/lib/DhanHQ/concerns/order_audit.rb +43 -1
- data/lib/DhanHQ/constants.rb +3 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
- data/lib/DhanHQ/mcp/server.rb +172 -9
- data/lib/DhanHQ/models/instrument.rb +44 -14
- data/lib/DhanHQ/rate_limiter.rb +5 -3
- data/lib/DhanHQ/resources/alert_orders.rb +1 -0
- data/lib/DhanHQ/resources/forever_orders.rb +1 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +1 -0
- data/lib/DhanHQ/resources/orders.rb +2 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
- data/lib/DhanHQ/resources/super_orders.rb +1 -0
- data/lib/DhanHQ/resources/twap_orders.rb +1 -0
- data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
- data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
- data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
- data/lib/DhanHQ/risk/pipeline.rb +8 -1
- data/lib/DhanHQ/skills/base.rb +54 -3
- data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +10 -13
- data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +15 -19
- data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
- data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +5 -8
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +8 -6
- data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +13 -13
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/dhan_hq.rb +47 -0
- data/skills/dhanhq-ruby/SKILL.md +174 -41
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
- data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
- data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
- data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
- data/skills/dhanhq-ruby/references/error-codes.md +50 -0
- data/skills/dhanhq-ruby/references/funds.md +67 -0
- data/skills/dhanhq-ruby/references/instruments.md +85 -0
- data/skills/dhanhq-ruby/references/live-feed.md +83 -0
- data/skills/dhanhq-ruby/references/market-data.md +119 -0
- data/skills/dhanhq-ruby/references/option-chain.md +71 -0
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
- data/skills/dhanhq-ruby/references/orders.md +200 -6
- data/skills/dhanhq-ruby/references/portfolio.md +93 -0
- data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
- data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
- metadata +39 -3
- data/skills/dhanhq-ruby/references/market_data.md +0 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a long straddle (buy ATM call + buy ATM put).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain →
|
|
9
|
+
# select ATM strikes → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("straddle",
|
|
13
|
+
# symbol: "NIFTY",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 25
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Straddle < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a long straddle: buy ATM call + buy ATM put at the same strike."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 25
|
|
26
|
+
param :stop_loss, type: :number, default: 300
|
|
27
|
+
param :target, type: :number, default: 600
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_atm_strikes, priority: 4
|
|
33
|
+
step :build_intent, priority: 5
|
|
34
|
+
|
|
35
|
+
def find_instrument(ctx)
|
|
36
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
37
|
+
ctx
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_spot_price(ctx)
|
|
41
|
+
ctx[:spot_price] = ctx[:instrument].ltp
|
|
42
|
+
ctx
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_option_chain(ctx)
|
|
46
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
47
|
+
ctx
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select_atm_strikes(ctx)
|
|
51
|
+
spot = ctx[:spot_price].to_f
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
|
|
54
|
+
atm = nearest_strike(chain, spot)
|
|
55
|
+
raise ArgumentError, "Could not find ATM strike" unless atm
|
|
56
|
+
|
|
57
|
+
ctx[:atm_strike] = atm[:strike]
|
|
58
|
+
ctx[:ce_security_id] = leg_security_id(atm, "CE")
|
|
59
|
+
ctx[:pe_security_id] = leg_security_id(atm, "PE")
|
|
60
|
+
ctx[:ce_premium] = leg_premium(atm, "CE")
|
|
61
|
+
ctx[:pe_premium] = leg_premium(atm, "PE")
|
|
62
|
+
ctx[:total_premium] = ctx[:ce_premium].to_f + ctx[:pe_premium].to_f
|
|
63
|
+
ctx
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_intent(ctx)
|
|
67
|
+
ctx[:intent] = {
|
|
68
|
+
trade_type: "STRADDLE",
|
|
69
|
+
symbol: ctx[:symbol],
|
|
70
|
+
expiry: ctx[:expiry],
|
|
71
|
+
quantity: ctx[:quantity],
|
|
72
|
+
legs: [
|
|
73
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: ctx[:atm_strike], security_id: ctx[:ce_security_id], premium: ctx[:ce_premium] },
|
|
74
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: ctx[:atm_strike], security_id: ctx[:pe_security_id], premium: ctx[:pe_premium] }
|
|
75
|
+
],
|
|
76
|
+
total_premium: ctx[:total_premium],
|
|
77
|
+
break_even_upside: ctx[:atm_strike].to_f + ctx[:total_premium],
|
|
78
|
+
break_even_downside: ctx[:atm_strike].to_f - ctx[:total_premium],
|
|
79
|
+
stop_loss: ctx[:stop_loss],
|
|
80
|
+
target: ctx[:target],
|
|
81
|
+
note: "Long straddle prepared. Await human confirmation."
|
|
82
|
+
}
|
|
83
|
+
ctx
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -16,6 +16,10 @@ module DhanHQ
|
|
|
16
16
|
# )
|
|
17
17
|
#
|
|
18
18
|
class Strangle < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a long strangle: buy OTM call + buy OTM put around the current spot price."
|
|
22
|
+
|
|
19
23
|
param :symbol, type: :string, required: true
|
|
20
24
|
param :expiry, type: :string, required: true
|
|
21
25
|
param :quantity, type: :integer, default: 50
|
|
@@ -35,8 +39,7 @@ module DhanHQ
|
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def get_spot_price(ctx)
|
|
38
|
-
|
|
39
|
-
ctx[:spot_price] = ltp[:ltp] || ltp["ltp"]
|
|
42
|
+
ctx[:spot_price] = ctx[:instrument].ltp
|
|
40
43
|
ctx
|
|
41
44
|
end
|
|
42
45
|
|
|
@@ -50,24 +53,21 @@ module DhanHQ
|
|
|
50
53
|
chain = ctx[:chain]
|
|
51
54
|
offset = ctx[:offset_pct] / 100.0
|
|
52
55
|
|
|
53
|
-
ce_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "CE" }
|
|
54
|
-
pe_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "PE" }
|
|
55
|
-
|
|
56
56
|
ce_strike = spot * (1 + offset)
|
|
57
57
|
pe_strike = spot * (1 - offset)
|
|
58
58
|
|
|
59
|
-
long_ce =
|
|
60
|
-
long_pe =
|
|
59
|
+
long_ce = nearest_strike(chain, ce_strike)
|
|
60
|
+
long_pe = nearest_strike(chain, pe_strike)
|
|
61
61
|
|
|
62
62
|
raise ArgumentError, "Could not find suitable CE strike near #{ce_strike}" unless long_ce
|
|
63
63
|
raise ArgumentError, "Could not find suitable PE strike near #{pe_strike}" unless long_pe
|
|
64
64
|
|
|
65
|
-
ctx[:ce_strike] = long_ce[:strike]
|
|
66
|
-
ctx[:pe_strike] = long_pe[:strike]
|
|
67
|
-
ctx[:ce_security_id] = long_ce
|
|
68
|
-
ctx[:pe_security_id] = long_pe
|
|
69
|
-
ctx[:ce_premium] = long_ce
|
|
70
|
-
ctx[:pe_premium] = long_pe
|
|
65
|
+
ctx[:ce_strike] = long_ce[:strike]
|
|
66
|
+
ctx[:pe_strike] = long_pe[:strike]
|
|
67
|
+
ctx[:ce_security_id] = leg_security_id(long_ce, "CE")
|
|
68
|
+
ctx[:pe_security_id] = leg_security_id(long_pe, "PE")
|
|
69
|
+
ctx[:ce_premium] = leg_premium(long_ce, "CE")
|
|
70
|
+
ctx[:pe_premium] = leg_premium(long_pe, "PE")
|
|
71
71
|
ctx
|
|
72
72
|
end
|
|
73
73
|
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/dhan_hq.rb
CHANGED
|
@@ -53,6 +53,9 @@ module DhanHQ
|
|
|
53
53
|
require_relative "DhanHQ/risk/checks/quantity"
|
|
54
54
|
require_relative "DhanHQ/risk/checks/market_hours"
|
|
55
55
|
require_relative "DhanHQ/risk/checks/options"
|
|
56
|
+
require_relative "DhanHQ/risk/checks/position_limits"
|
|
57
|
+
require_relative "DhanHQ/risk/checks/concentration"
|
|
58
|
+
require_relative "DhanHQ/risk/checks/max_loss"
|
|
56
59
|
require_relative "DhanHQ/risk/pipeline"
|
|
57
60
|
|
|
58
61
|
# Skills layer: multi-step trading workflows
|
|
@@ -65,6 +68,7 @@ module DhanHQ
|
|
|
65
68
|
require_relative "DhanHQ/skills/builtin/square_off_position"
|
|
66
69
|
require_relative "DhanHQ/skills/builtin/iron_condor"
|
|
67
70
|
require_relative "DhanHQ/skills/builtin/strangle"
|
|
71
|
+
require_relative "DhanHQ/skills/builtin/market_data_summarizer"
|
|
68
72
|
DhanHQ::Skills::Registry.load_builtins
|
|
69
73
|
|
|
70
74
|
class Error < StandardError; end
|
|
@@ -186,6 +190,49 @@ module DhanHQ
|
|
|
186
190
|
configuration
|
|
187
191
|
end
|
|
188
192
|
|
|
193
|
+
# Configures the DhanHQ client by fetching credentials from a dashboard API.
|
|
194
|
+
#
|
|
195
|
+
# @param bearer_token [String] Secret dashboard token (e.g. YOUR_DASHBOARD_TOKEN)
|
|
196
|
+
# @param url [String] The full URL of the dashboard API
|
|
197
|
+
# @return [DhanHQ::Configuration] The configured configuration
|
|
198
|
+
# @raise [DhanHQ::TokenEndpointError] On HTTP error or missing credentials
|
|
199
|
+
def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
|
|
200
|
+
raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
|
|
201
|
+
|
|
202
|
+
conn = ::Faraday.new(url: url) do |c|
|
|
203
|
+
c.request :url_encoded
|
|
204
|
+
c.adapter ::Faraday.default_adapter
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
response = conn.get("") do |req|
|
|
208
|
+
req.headers["Authorization"] = "Bearer #{bearer_token}"
|
|
209
|
+
req.headers["Accept"] = "application/json"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
unless response.success?
|
|
213
|
+
body = parse_json_body(response.body)
|
|
214
|
+
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
215
|
+
raise DhanHQ::TokenEndpointError, "Dashboard returned #{response.status}: #{msg}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
data = parse_json_body(response.body)
|
|
219
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
220
|
+
|
|
221
|
+
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
222
|
+
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
223
|
+
client_id ||= self.configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
224
|
+
|
|
225
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
226
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
227
|
+
|
|
228
|
+
self.configuration ||= Configuration.new
|
|
229
|
+
configuration.access_token = access_token.to_s
|
|
230
|
+
configuration.client_id = client_id.to_s
|
|
231
|
+
dhan_base = data["base_url"] || data["baseUrl"]
|
|
232
|
+
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
233
|
+
configuration
|
|
234
|
+
end
|
|
235
|
+
|
|
189
236
|
# @param body [String, Hash] Raw response body
|
|
190
237
|
# @return [Hash] Parsed hash; empty hash on parse failure or empty string
|
|
191
238
|
def parse_json_body(body)
|
data/skills/dhanhq-ruby/SKILL.md
CHANGED
|
@@ -1,74 +1,207 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: dhanhq-ruby
|
|
3
|
+
description: >
|
|
4
|
+
Use when the user mentions DhanHQ, Dhan API, or wants to trade on
|
|
5
|
+
Indian exchanges (NSE, BSE, MCX) using Ruby. Triggers for: place, modify, or
|
|
6
|
+
cancel stock/F&O/commodity orders on Dhan using Ruby; fetch portfolio holdings
|
|
7
|
+
or positions; get live or historical market data; access option
|
|
8
|
+
chains with Greeks; check fund limits or margin; build any trading
|
|
9
|
+
automation for Indian markets; resolve NSE/BSE instrument IDs;
|
|
10
|
+
stream live WebSocket market feeds or order updates. Also trigger
|
|
11
|
+
for general questions about programmatic trading on Indian exchanges
|
|
12
|
+
if Dhan is the user's broker.
|
|
13
|
+
compatibility: >
|
|
14
|
+
Requires Ruby and the dhanhq gem.
|
|
15
|
+
Order placement, modification, and cancellation require static IP
|
|
16
|
+
whitelisting on Dhan. Data APIs (quotes, history, option chain,
|
|
17
|
+
live feed) require an active Dhan Data Plan.
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# DhanHQ — Indian Market Trading Skill (Ruby SDK)
|
|
2
21
|
|
|
3
22
|
Use this skill when an agent needs to write, review, or operate Ruby code using the `DhanHQ` gem.
|
|
4
23
|
|
|
5
|
-
## Safety
|
|
24
|
+
## Safety Rules — Always Enforce
|
|
6
25
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
1. Confirm before placing live orders.
|
|
27
|
+
2. Show a readable order preview before execution.
|
|
28
|
+
3. Default to `LIMIT` orders unless the user explicitly wants `MARKET`.
|
|
29
|
+
4. Warn when notional exceeds `Rs. 50,000`.
|
|
30
|
+
5. For F&O, validate lot size before placement.
|
|
31
|
+
6. Never use `CNC` or `MTF` for F&O, commodity, or currency segments.
|
|
32
|
+
7. Never hardcode credentials in generated code.
|
|
33
|
+
8. Ask for confirmation before modifying or cancelling orders, or performing multi-leg live execution.
|
|
13
34
|
|
|
14
35
|
## Setup
|
|
15
36
|
|
|
37
|
+
Require the core library and configure using environment variables or a configuration block:
|
|
38
|
+
|
|
16
39
|
```ruby
|
|
17
40
|
require "dhan_hq"
|
|
18
|
-
require "dhan_hq/agent"
|
|
19
41
|
|
|
42
|
+
# Configures from environment variables: DHAN_CLIENT_ID, DHAN_ACCESS_TOKEN
|
|
20
43
|
DhanHQ.configure_with_env
|
|
21
44
|
```
|
|
22
45
|
|
|
23
|
-
|
|
46
|
+
Or configure explicitly:
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
```ruby
|
|
49
|
+
DhanHQ.configure do |config|
|
|
50
|
+
config.client_id = "YOUR_CLIENT_ID"
|
|
51
|
+
config.access_token = "YOUR_ACCESS_TOKEN"
|
|
52
|
+
end
|
|
53
|
+
```
|
|
27
54
|
|
|
28
|
-
|
|
55
|
+
If generating scripts inside this repo, prefer using the helper script:
|
|
29
56
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
```ruby
|
|
58
|
+
require_relative "scripts/dhan_helpers"
|
|
59
|
+
get_client
|
|
60
|
+
```
|
|
33
61
|
|
|
34
|
-
##
|
|
62
|
+
## Current SDK Constants
|
|
63
|
+
|
|
64
|
+
Use constants from the `DhanHQ::Constants` module for segments, order types, validity, product types, and transactions:
|
|
65
|
+
|
|
66
|
+
| Category | Constant | Value |
|
|
67
|
+
|----------|----------|-------|
|
|
68
|
+
| Exchange Segment | `DhanHQ::Constants::ExchangeSegment::NSE_EQ` (or `DhanHQ::Constants::NSE`) | `"NSE_EQ"` |
|
|
69
|
+
| | `DhanHQ::Constants::ExchangeSegment::BSE_EQ` (or `DhanHQ::Constants::BSE`) | `"BSE_EQ"` |
|
|
70
|
+
| | `DhanHQ::Constants::ExchangeSegment::NSE_FNO` (or `DhanHQ::Constants::NSE_FNO` / `DhanHQ::Constants::FNO`) | `"NSE_FNO"` |
|
|
71
|
+
| | `DhanHQ::Constants::ExchangeSegment::BSE_FNO` (or `DhanHQ::Constants::BSE_FNO`) | `"BSE_FNO"` |
|
|
72
|
+
| | `DhanHQ::Constants::ExchangeSegment::MCX_COMM` (or `DhanHQ::Constants::MCX`) | `"MCX_COMM"` |
|
|
73
|
+
| | `DhanHQ::Constants::ExchangeSegment::IDX_I` (or `DhanHQ::Constants::INDEX`) | `"IDX_I"` |
|
|
74
|
+
| Transaction Type | `DhanHQ::Constants::TransactionType::BUY` (or `DhanHQ::Constants::BUY`) | `"BUY"` |
|
|
75
|
+
| | `DhanHQ::Constants::TransactionType::SELL` (or `DhanHQ::Constants::SELL`) | `"SELL"` |
|
|
76
|
+
| Order Type | `DhanHQ::Constants::OrderType::LIMIT` (or `DhanHQ::Constants::LIMIT`) | `"LIMIT"` |
|
|
77
|
+
| | `DhanHQ::Constants::OrderType::MARKET` (or `DhanHQ::Constants::MARKET`) | `"MARKET"` |
|
|
78
|
+
| | `DhanHQ::Constants::OrderType::STOP_LOSS` (or `DhanHQ::Constants::SL`) | `"STOP_LOSS"` |
|
|
79
|
+
| | `DhanHQ::Constants::OrderType::STOP_LOSS_MARKET` (or `DhanHQ::Constants::SLM`) | `"STOP_LOSS_MARKET"` |
|
|
80
|
+
| Product Type | `DhanHQ::Constants::ProductType::CNC` (or `DhanHQ::Constants::CNC`) | `"CNC"` |
|
|
81
|
+
| | `DhanHQ::Constants::ProductType::INTRADAY` (or `DhanHQ::Constants::INTRA`) | `"INTRADAY"` |
|
|
82
|
+
| | `DhanHQ::Constants::ProductType::MARGIN` (or `DhanHQ::Constants::MARGIN`) | `"MARGIN"` |
|
|
83
|
+
| | `DhanHQ::Constants::ProductType::MTF` (or `DhanHQ::Constants::MTF`) | `"MTF"` |
|
|
84
|
+
| Validity | `DhanHQ::Constants::Validity::DAY` (or `DhanHQ::Constants::DAY`) | `"DAY"` |
|
|
85
|
+
| | `DhanHQ::Constants::Validity::IOC` (or `DhanHQ::Constants::IOC`) | `"IOC"` |
|
|
86
|
+
|
|
87
|
+
## Preferred SDK Methods (ActiveRecord-Style Models)
|
|
88
|
+
|
|
89
|
+
| Task | Method |
|
|
90
|
+
|------|--------|
|
|
91
|
+
| Place order | `DhanHQ::Models::Order.place(params)` |
|
|
92
|
+
| List all orders | `DhanHQ::Models::Order.all` |
|
|
93
|
+
| Order by ID | `DhanHQ::Models::Order.find(order_id)` |
|
|
94
|
+
| Order by correlation ID | `DhanHQ::Models::Order.find_by_correlation(correlation_id)` |
|
|
95
|
+
| Today's trades | `DhanHQ::Models::Trade.today` |
|
|
96
|
+
| Historical trades | `DhanHQ::Models::Trade.history(from_date:, to_date:, page:)` |
|
|
97
|
+
| Holdings | `DhanHQ::Models::Holding.all` |
|
|
98
|
+
| Positions | `DhanHQ::Models::Position.all` |
|
|
99
|
+
| Fund limits | `DhanHQ::Models::Funds.fetch` |
|
|
100
|
+
| Margin calculator | `DhanHQ::Models::Margin.calculate(params)` |
|
|
101
|
+
| Multi-instrument margin | `DhanHQ::Models::Margin.calculate_multi(params)` |
|
|
102
|
+
| Daily charts | `DhanHQ::Models::HistoricalData.daily(params)` |
|
|
103
|
+
| Intraday charts | `DhanHQ::Models::HistoricalData.intraday(params)` |
|
|
104
|
+
| Option chain | `DhanHQ::Models::OptionChain.fetch(params)` |
|
|
105
|
+
| Expiry list | `DhanHQ::Models::OptionChain.fetch_expiry_list(params)` |
|
|
106
|
+
| Search instruments | `DhanHQ::Models::Instrument.search(query, options)` |
|
|
107
|
+
| Find specific instrument | `DhanHQ::Models::Instrument.find(exchange_segment, symbol, options)` |
|
|
108
|
+
| Find instrument anywhere | `DhanHQ::Models::Instrument.find_anywhere(symbol, options)` |
|
|
109
|
+
| Super orders | `DhanHQ::Models::SuperOrder.create(params)` |
|
|
110
|
+
| Forever orders | `DhanHQ::Models::ForeverOrder.create(params)` |
|
|
111
|
+
| Live market feed | `DhanHQ::WS.connect(mode: :ticker) { |tick| ... }` |
|
|
112
|
+
| Live order updates | `DhanHQ::WS::Orders.connect { |update| ... }` |
|
|
113
|
+
| Live market depth | `DhanHQ::WS::MarketDepth.connect(symbols: [{...}]) { |depth| ... }` |
|
|
114
|
+
|
|
115
|
+
## High-Value Gotchas
|
|
116
|
+
|
|
117
|
+
- **Return values are model instances:** Most class methods return `DhanHQ::Models` objects rather than raw HTTP hashes.
|
|
118
|
+
- **Instrument search is segment-specific:** `DhanHQ::Models::Instrument.by_segment(exchange_segment)` downloads the CSV for a single segment, which is more token-efficient than downloading the entire master CSV.
|
|
119
|
+
- **Spelling fixes in models:** Typos from the Dhan API are normalized in model attributes (e.g. `availabelBalance` is normalized to `available_balance` or `availabel_balance`).
|
|
120
|
+
- **Timestamps:** Timestamps returned by `HistoricalData` are automatically normalized into Ruby `Time` objects.
|
|
121
|
+
- **WebSocket connection management:** Use sequential connections or single connection pools to avoid 429 rate limiting. Dhan allows up to 5 concurrent WebSocket connections.
|
|
122
|
+
|
|
123
|
+
## Core Patterns
|
|
124
|
+
|
|
125
|
+
### 1. Check account access before data calls
|
|
35
126
|
|
|
36
127
|
```ruby
|
|
37
|
-
DhanHQ::Models::
|
|
38
|
-
|
|
39
|
-
DhanHQ::Models::Holding.all
|
|
40
|
-
DhanHQ::Models::Position.all
|
|
41
|
-
DhanHQ::Models::Order.all
|
|
42
|
-
DhanHQ::Models::Trade.today
|
|
43
|
-
DhanHQ::Models::Instrument.search("RELIANCE", segments: ["NSE_EQ"], limit: 5)
|
|
44
|
-
DhanHQ::Models::MarketFeed.ltp("NSE_EQ" => ["2885"])
|
|
128
|
+
funds = DhanHQ::Models::Funds.fetch
|
|
129
|
+
puts "Available Balance: Rs. #{funds.availabel_balance}"
|
|
45
130
|
```
|
|
46
131
|
|
|
47
|
-
|
|
132
|
+
### 2. Fetch historical data
|
|
48
133
|
|
|
49
134
|
```ruby
|
|
50
|
-
|
|
51
|
-
transaction_type: "BUY",
|
|
52
|
-
exchange_segment: "NSE_EQ",
|
|
53
|
-
product_type: "INTRADAY",
|
|
54
|
-
order_type: "MARKET",
|
|
55
|
-
validity: "DAY",
|
|
135
|
+
candles = DhanHQ::Models::HistoricalData.daily(
|
|
56
136
|
security_id: "2885",
|
|
57
|
-
|
|
58
|
-
|
|
137
|
+
exchange_segment: "NSE_EQ",
|
|
138
|
+
instrument: "EQUITY",
|
|
139
|
+
from_date: "2024-01-01",
|
|
140
|
+
to_date: "2024-12-31"
|
|
59
141
|
)
|
|
60
142
|
|
|
61
|
-
|
|
143
|
+
candles.each do |candle|
|
|
144
|
+
puts "Date: #{candle[:timestamp].to_date}, Close: #{candle[:close]}"
|
|
145
|
+
end
|
|
62
146
|
```
|
|
63
147
|
|
|
64
|
-
|
|
148
|
+
### 3. Option-chain data
|
|
65
149
|
|
|
66
|
-
|
|
150
|
+
```ruby
|
|
151
|
+
chain = DhanHQ::Models::OptionChain.fetch(
|
|
152
|
+
underlying_scrip: 13,
|
|
153
|
+
underlying_seg: "IDX_I",
|
|
154
|
+
expiry: "2025-03-27"
|
|
155
|
+
)
|
|
67
156
|
|
|
68
|
-
|
|
69
|
-
|
|
157
|
+
puts "Underlying spot: #{chain[:last_price]}"
|
|
158
|
+
chain[:strikes].each do |strike_data|
|
|
159
|
+
puts "Strike: #{strike_data[:strike]}, Call LTP: #{strike_data[:call][:last_price]}"
|
|
160
|
+
end
|
|
70
161
|
```
|
|
71
162
|
|
|
72
|
-
|
|
163
|
+
### 4. Margin check before live order placement
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
margin = DhanHQ::Models::Margin.calculate(
|
|
167
|
+
security_id: "2885",
|
|
168
|
+
exchange_segment: "NSE_EQ",
|
|
169
|
+
transaction_type: "BUY",
|
|
170
|
+
quantity: 10,
|
|
171
|
+
product_type: "CNC",
|
|
172
|
+
price: 2450.0
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
puts "Sufficient balance? #{margin.available_balance >= margin.total_margin}"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5. Live market feed
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
|
182
|
+
puts "Market Tick: #{tick[:security_id]} = #{tick[:ltp]}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
market_client.subscribe_one(segment: "NSE_EQ", security_id: "2885")
|
|
186
|
+
sleep(10)
|
|
187
|
+
market_client.stop
|
|
188
|
+
```
|
|
73
189
|
|
|
74
|
-
|
|
190
|
+
## Reference Files
|
|
191
|
+
|
|
192
|
+
Refer to the documents in the references directory for focused workflows:
|
|
193
|
+
|
|
194
|
+
| Need | File |
|
|
195
|
+
|------|------|
|
|
196
|
+
| Orders, super orders, forever orders | [references/orders.md](references/orders.md) |
|
|
197
|
+
| Holdings, positions, eDIS | [references/portfolio.md](references/portfolio.md) |
|
|
198
|
+
| Daily/minute history, quotes, expired options | [references/market-data.md](references/market-data.md) |
|
|
199
|
+
| Option-chain usage | [references/option-chain.md](references/option-chain.md) |
|
|
200
|
+
| Fund limits and margin checks | [references/funds.md](references/funds.md) |
|
|
201
|
+
| Live feeds and depth | [references/live-feed.md](references/live-feed.md) |
|
|
202
|
+
| Error handling | [references/error-codes.md](references/error-codes.md) |
|
|
203
|
+
| Instrument resolution | [references/instruments.md](references/instruments.md) |
|
|
204
|
+
| Multi-step execution patterns | [references/common-workflows.md](references/common-workflows.md) |
|
|
205
|
+
| Options analytics | [references/options-analysis-patterns.md](references/options-analysis-patterns.md) |
|
|
206
|
+
| Backtesting patterns | [references/backtesting-with-dhan.md](references/backtesting-with-dhan.md) |
|
|
207
|
+
| Extranal data sources (RSI, PE ratios, screener) | [references/scanx-data.md](references/scanx-data.md) |
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
4
|
+
require "dhan_hq"
|
|
5
|
+
require_relative "../scripts/dhan_helpers"
|
|
6
|
+
|
|
7
|
+
# Initialize credentials
|
|
8
|
+
get_client
|
|
9
|
+
|
|
10
|
+
expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(underlying_scrip: 13, underlying_seg: DhanHQ::Constants::ExchangeSegment::IDX_I)
|
|
11
|
+
nearest_expiry = expiries.first
|
|
12
|
+
|
|
13
|
+
if nearest_expiry.nil?
|
|
14
|
+
puts "Failed to fetch expiries."
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts "Using expiry: #{nearest_expiry}"
|
|
19
|
+
|
|
20
|
+
chain_df, spot = fetch_chain_df(under_security_id: 13, expiry: nearest_expiry)
|
|
21
|
+
atm = find_atm_row(chain_df, spot)
|
|
22
|
+
|
|
23
|
+
if atm.nil?
|
|
24
|
+
puts "Failed to find ATM row."
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "Nifty Spot: #{spot}"
|
|
29
|
+
puts "ATM Strike: #{atm["strike"]}"
|
|
30
|
+
|
|
31
|
+
# Filter strikes between ATM - 500 and ATM + 500
|
|
32
|
+
nearby = chain_df.select do |row|
|
|
33
|
+
row["strike"].between?(atm["strike"] - 500, atm["strike"] + 500)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "\nOption Chain (ATM ± 500 points):\n\n"
|
|
37
|
+
printf(
|
|
38
|
+
"%-10s | %-8s | %-12s | %-6s | %-8s | %-12s | %-6s\n",
|
|
39
|
+
"Strike", "CE LTP", "CE OI", "CE IV", "PE LTP", "PE OI", "PE IV"
|
|
40
|
+
)
|
|
41
|
+
puts "-" * 75
|
|
42
|
+
nearby.each do |row|
|
|
43
|
+
printf(
|
|
44
|
+
"%-10g | %-8.2f | %-12d | %-6.2f | %-8.2f | %-12d | %-6.2f\n",
|
|
45
|
+
row["strike"],
|
|
46
|
+
row["ce_ltp"].to_f,
|
|
47
|
+
row["ce_oi"].to_i,
|
|
48
|
+
row["ce_iv"].to_f,
|
|
49
|
+
row["pe_ltp"].to_f,
|
|
50
|
+
row["pe_oi"].to_i,
|
|
51
|
+
row["pe_iv"].to_f
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
puts
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
4
|
+
require "dhan_hq"
|
|
5
|
+
require_relative "../scripts/dhan_helpers"
|
|
6
|
+
|
|
7
|
+
# Initialize credentials
|
|
8
|
+
get_client
|
|
9
|
+
|
|
10
|
+
# Example 1: Single GTT — Buy Reliance if it dips to ₹2300
|
|
11
|
+
puts "--- GTT Single: Buy RELIANCE on dip ---"
|
|
12
|
+
# order = DhanHQ::Models::ForeverOrder.create(
|
|
13
|
+
# security_id: "2885",
|
|
14
|
+
# exchange_segment: "NSE_EQ",
|
|
15
|
+
# transaction_type: "BUY",
|
|
16
|
+
# product_type: "CNC", # Equity delivery
|
|
17
|
+
# order_type: "LIMIT",
|
|
18
|
+
# quantity: 5,
|
|
19
|
+
# price: 2300.00, # Limit price
|
|
20
|
+
# trigger_price: 2305.00, # Trigger price
|
|
21
|
+
# order_flag: "SINGLE",
|
|
22
|
+
# validity: "DAY"
|
|
23
|
+
# )
|
|
24
|
+
# puts "GTT placed: #{order.inspect}"
|
|
25
|
+
|
|
26
|
+
# Example 2: OCO — Sell Reliance at ₹2700 (target) OR ₹2200 (stop loss)
|
|
27
|
+
puts "\n--- GTT OCO: Target + Stop Loss for RELIANCE holding ---"
|
|
28
|
+
# order = DhanHQ::Models::ForeverOrder.create(
|
|
29
|
+
# security_id: "2885",
|
|
30
|
+
# exchange_segment: "NSE_EQ",
|
|
31
|
+
# transaction_type: "SELL",
|
|
32
|
+
# product_type: "CNC", # Selling from holdings
|
|
33
|
+
# order_type: "LIMIT",
|
|
34
|
+
# quantity: 5,
|
|
35
|
+
# price: 2700.00, # Target price (price of first leg)
|
|
36
|
+
# trigger_price: 2695.00, # Target trigger price (trigger of first leg)
|
|
37
|
+
# price1: 2200.00, # Stop loss price (price of second leg)
|
|
38
|
+
# trigger_price1: 2205.00, # Stop loss trigger price (trigger of second leg)
|
|
39
|
+
# quantity1: 5, # Stop loss quantity (quantity of second leg)
|
|
40
|
+
# order_flag: "OCO", # One Cancels Other
|
|
41
|
+
# validity: "DAY"
|
|
42
|
+
# )
|
|
43
|
+
# puts "OCO placed: #{order.inspect}"
|
|
44
|
+
|
|
45
|
+
# Example 3: List all active forever orders
|
|
46
|
+
puts "\n--- Active Forever Orders ---"
|
|
47
|
+
forever_orders = begin
|
|
48
|
+
DhanHQ::Models::ForeverOrder.all
|
|
49
|
+
rescue StandardError
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
if forever_orders.any?
|
|
53
|
+
forever_orders.each do |ord|
|
|
54
|
+
puts " ID: #{ord.order_id} | " \
|
|
55
|
+
"#{ord.trading_symbol} | " \
|
|
56
|
+
"Type: #{ord.order_flag} | " \
|
|
57
|
+
"Trigger: ₹#{ord.trigger_price}"
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
puts " No active forever orders"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Example 4: Cancel a forever order
|
|
64
|
+
# order = DhanHQ::Models::ForeverOrder.find("YOUR_ORDER_ID")
|
|
65
|
+
# order.cancel if order
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
4
|
+
require "dhan_hq"
|
|
5
|
+
require "date"
|
|
6
|
+
require_relative "../scripts/dhan_helpers"
|
|
7
|
+
|
|
8
|
+
# Initialize credentials
|
|
9
|
+
get_client
|
|
10
|
+
|
|
11
|
+
to_date = Date.today.strftime("%Y-%m-%d")
|
|
12
|
+
from_date = (Date.today - 180).strftime("%Y-%m-%d")
|
|
13
|
+
|
|
14
|
+
# Fetch daily charts via HistoricalData model
|
|
15
|
+
candles = DhanHQ::Models::HistoricalData.daily(
|
|
16
|
+
security_id: "2885",
|
|
17
|
+
exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
|
|
18
|
+
instrument: DhanHQ::Constants::InstrumentType::EQUITY,
|
|
19
|
+
from_date: from_date,
|
|
20
|
+
to_date: to_date
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if candles.empty?
|
|
24
|
+
puts "No candle data returned."
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
close_prices = candles.map { |c| c[:close].to_f }
|
|
29
|
+
timestamps = candles.map { |c| c[:timestamp] }
|
|
30
|
+
|
|
31
|
+
# Calculate SMAs
|
|
32
|
+
def calculate_sma(prices, period)
|
|
33
|
+
return [] if prices.size < period
|
|
34
|
+
|
|
35
|
+
# Calculate SMA for each index starting from period - 1
|
|
36
|
+
((period - 1)...prices.size).map do |i|
|
|
37
|
+
prices[(i - period + 1)..i].sum / period.to_f
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sma_20_series = calculate_sma(close_prices, 20)
|
|
42
|
+
sma_50_series = calculate_sma(close_prices, 50)
|
|
43
|
+
|
|
44
|
+
latest_sma_20 = sma_20_series.last
|
|
45
|
+
latest_sma_50 = sma_50_series.last
|
|
46
|
+
|
|
47
|
+
# Calculate returns
|
|
48
|
+
returns = []
|
|
49
|
+
close_prices.each_cons(2) do |prev_price, curr_price|
|
|
50
|
+
returns << ((curr_price - prev_price) / prev_price)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Calculate daily volatility (standard deviation of returns) and annualize it
|
|
54
|
+
mean_return = returns.sum / returns.size.to_f
|
|
55
|
+
variance = returns.sum { |r| (r - mean_return)**2 } / (returns.size - 1).to_f
|
|
56
|
+
std_dev = Math.sqrt(variance)
|
|
57
|
+
annualized_volatility = std_dev * Math.sqrt(252)
|
|
58
|
+
|
|
59
|
+
start_date = begin
|
|
60
|
+
timestamps.first.is_a?(Time) ? timestamps.first.to_date : Date.parse(timestamps.first.to_s)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
"N/A"
|
|
63
|
+
end
|
|
64
|
+
end_date = begin
|
|
65
|
+
timestamps.last.is_a?(Time) ? timestamps.last.to_date : Date.parse(timestamps.last.to_s)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
"N/A"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
puts "=== RELIANCE — Last 6 Months ===\n\n"
|
|
71
|
+
puts "Period: #{start_date} to #{end_date}"
|
|
72
|
+
puts "Trading Days: #{candles.size}"
|
|
73
|
+
puts "Start Price: Rs. #{"%.2f" % close_prices.first}"
|
|
74
|
+
puts "End Price: Rs. #{"%.2f" % close_prices.last}"
|
|
75
|
+
puts "High: Rs. #{"%.2f" % candles.map { |c| c[:high].to_f }.max}"
|
|
76
|
+
puts "Low: Rs. #{"%.2f" % candles.map { |c| c[:low].to_f }.min}"
|
|
77
|
+
puts "Total Return: #{format("%.2f%", ((close_prices.last / close_prices.first) - 1.0) * 100)}"
|
|
78
|
+
puts "Avg Daily Vol: #{format("%.0f", candles.sum { |c| c[:volume].to_i } / candles.size.to_f)}"
|
|
79
|
+
puts "Volatility: #{format("%.2f%", annualized_volatility * 100)} (annualized)"
|
|
80
|
+
|
|
81
|
+
if latest_sma_20 && latest_sma_50
|
|
82
|
+
puts "\nSMA 20: Rs. #{"%.2f" % latest_sma_20}"
|
|
83
|
+
puts "SMA 50: Rs. #{"%.2f" % latest_sma_50}"
|
|
84
|
+
if latest_sma_20 > latest_sma_50
|
|
85
|
+
puts "Signal: Bullish (SMA 20 > SMA 50)"
|
|
86
|
+
else
|
|
87
|
+
puts "Signal: Bearish (SMA 20 < SMA 50)"
|
|
88
|
+
end
|
|
89
|
+
end
|