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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +152 -4
  5. data/docs/CONSTANTS_REFERENCE.md +3 -2
  6. data/exe/dhanhq-mcp +7 -0
  7. data/lib/DhanHQ/agent/tool_registry.rb +51 -2
  8. data/lib/DhanHQ/concerns/order_audit.rb +43 -1
  9. data/lib/DhanHQ/constants.rb +3 -2
  10. data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
  11. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +1 -1
  12. data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
  13. data/lib/DhanHQ/contracts/twap_order_contract.rb +1 -1
  14. data/lib/DhanHQ/mcp/server.rb +172 -9
  15. data/lib/DhanHQ/models/instrument.rb +44 -14
  16. data/lib/DhanHQ/rate_limiter.rb +5 -3
  17. data/lib/DhanHQ/resources/alert_orders.rb +1 -0
  18. data/lib/DhanHQ/resources/forever_orders.rb +1 -0
  19. data/lib/DhanHQ/resources/iceberg_orders.rb +1 -0
  20. data/lib/DhanHQ/resources/orders.rb +2 -0
  21. data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
  22. data/lib/DhanHQ/resources/super_orders.rb +1 -0
  23. data/lib/DhanHQ/resources/twap_orders.rb +1 -0
  24. data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
  25. data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
  26. data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
  27. data/lib/DhanHQ/risk/pipeline.rb +8 -1
  28. data/lib/DhanHQ/skills/base.rb +54 -3
  29. data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
  30. data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
  31. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +10 -13
  32. data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
  33. data/lib/DhanHQ/skills/builtin/iron_condor.rb +15 -19
  34. data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
  35. data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
  36. data/lib/DhanHQ/skills/builtin/square_off_all.rb +5 -8
  37. data/lib/DhanHQ/skills/builtin/square_off_position.rb +8 -6
  38. data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
  39. data/lib/DhanHQ/skills/builtin/strangle.rb +13 -13
  40. data/lib/DhanHQ/version.rb +1 -1
  41. data/lib/dhan_hq.rb +47 -0
  42. data/skills/dhanhq-ruby/SKILL.md +174 -41
  43. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  44. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  45. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  46. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  47. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  48. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  49. data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
  50. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  51. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  52. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  53. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  54. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  55. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  56. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  57. data/skills/dhanhq-ruby/references/funds.md +67 -0
  58. data/skills/dhanhq-ruby/references/instruments.md +85 -0
  59. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  60. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  61. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  62. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  63. data/skills/dhanhq-ruby/references/orders.md +200 -6
  64. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  65. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  66. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  67. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  68. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  69. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  70. metadata +39 -3
  71. 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
- ltp = ctx[:instrument].ltp
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 = ce_options.min_by { |o| ((o[:strike] || o["strike"]).to_f - ce_strike).abs }
60
- long_pe = pe_options.min_by { |o| ((o[:strike] || o["strike"]).to_f - pe_strike).abs }
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] || long_ce["strike"]
66
- ctx[:pe_strike] = long_pe[:strike] || long_pe["strike"]
67
- ctx[:ce_security_id] = long_ce[:security_id] || long_ce["securityId"]
68
- ctx[:pe_security_id] = long_pe[:security_id] || long_pe["securityId"]
69
- ctx[:ce_premium] = long_ce[:last_price] || long_ce["lastPrice"] || long_ce[:ltp]
70
- ctx[:pe_premium] = long_pe[:last_price] || long_pe["lastPrice"] || long_pe[:ltp]
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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.0.1"
5
+ VERSION = "3.1.0"
6
6
  end
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)
@@ -1,74 +1,207 @@
1
- # DhanHQ Ruby SDK Skill
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 rules
24
+ ## Safety Rules — Always Enforce
6
25
 
7
- - Prefer read-only tools and SDK calls unless the user explicitly asks to trade.
8
- - Resolve instruments before trading with `DhanHQ::Models::Instrument.search` and use the returned `security_id` plus `exchange_segment`.
9
- - Preview every order with `DhanHQ::Agent::OrderPreview` before any live order placement.
10
- - Never place, modify, or cancel orders unless both `DHANHQ_MCP_ENABLE_WRITES=true` and `LIVE_TRADING=true` are set and the user has clearly confirmed the action.
11
- - Include a `correlation_id` for every agent-originated order.
12
- - Do not ask for or print access tokens.
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
- Required environment variables:
46
+ Or configure explicitly:
24
47
 
25
- - `DHAN_CLIENT_ID`
26
- - `DHAN_ACCESS_TOKEN`
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
- Optional agent variables:
55
+ If generating scripts inside this repo, prefer using the helper script:
29
56
 
30
- - `DHANHQ_AGENT_SCOPES`, comma-separated scopes such as `portfolio:read,market:read,orders:read`
31
- - `DHANHQ_MCP_ENABLE_WRITES=true` for agent write tools
32
- - `LIVE_TRADING=true` for any live trading write
57
+ ```ruby
58
+ require_relative "scripts/dhan_helpers"
59
+ get_client
60
+ ```
33
61
 
34
- ## Common calls
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::Profile.fetch
38
- DhanHQ::Models::Funds.fetch
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
- ## Order preview
132
+ ### 2. Fetch historical data
48
133
 
49
134
  ```ruby
50
- preview = DhanHQ::Agent::OrderPreview.new(
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
- quantity: 1,
58
- correlation_id: "agent-20260702-001"
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
- preview.to_h
143
+ candles.each do |candle|
144
+ puts "Date: #{candle[:timestamp].to_date}, Close: #{candle[:close]}"
145
+ end
62
146
  ```
63
147
 
64
- ## MCP server
148
+ ### 3. Option-chain data
65
149
 
66
- The gem includes a stdio MCP executable:
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
- ```bash
69
- dhanhq-mcp
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
- Start with read-only scopes. Add write scopes only for explicitly confirmed trading sessions.
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
- See the references directory for focused workflows.
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