DhanHQ — The Ruby SDK for Dhan API v2

Gem Version CI Ruby License: MIT

Build trading systems in Ruby without fighting raw HTTP, fragile auth flows, or unreliable market streams.

DhanHQ is a production-grade Ruby SDK for the Dhan trading API, designed for:

  • trading bots
  • real-time market data streaming
  • portfolio and order management
  • Rails or standalone trading systems

If you're looking for a Ruby SDK for Dhan API, this is built to be the default choice.

Unlike thin wrappers, DhanHQ gives you:

  • typed models for orders, positions, holdings, and more
  • WebSocket clients with auto-reconnect and backoff
  • token lifecycle management with retry-on-401
  • safety rails for live trading

This is closer to trading infrastructure than a simple API client.

Install and Run in 60 Seconds

# Gemfile
gem 'DhanHQ'
require 'dhan_hq'

DhanHQ.configure do |c|
  c.client_id    = ENV["DHAN_CLIENT_ID"]
  c.access_token = ENV["DHAN_ACCESS_TOKEN"]
end

# You're live — no manual HTTP, no JSON parsing
positions = DhanHQ::Models::Position.all

Who This Is For

  • Ruby developers building trading bots
  • Rails apps integrating the Dhan API
  • Algo trading systems that need clean abstractions over raw HTTP
  • Long-running processes that rely on WebSocket market data

Who This Is Not For

  • One-off scripts where raw HTTP is enough
  • Non-Ruby stacks

Start Here (Pick Your Use Case)

Pick the path that matches what you want to build:


Trust Signals

  • CI on supported Rubies — GitHub Actions runs RSpec on Ruby 3.2.0 and 3.3.4, plus RuboCop on every push and pull request
  • Typed domain models — Orders, Positions, Holdings, Funds, MarketFeed, OptionChain, Super Orders, and more expose a Ruby-first API instead of raw hashes
  • No real API calls in the default test suite — WebMock blocks outbound HTTP and VCR covers cassette-backed integration paths
  • Auth lifecycle support — static tokens, dynamic token providers, 401 retry with refresh hooks, and token sanitization in logs
  • WebSocket resilience — reconnect, backoff, 429 cool-off, local connection cleanup, and dedicated market/order stream clients
  • Live trading guardrails — order placement is blocked unless LIVE_TRADING=true, and order attempts emit structured audit logs

Why Not a Thin Wrapper?

Most API clients give you HTTP access. DhanHQ gives you a working Ruby system.

Instead of You get
JSON parsing and manual field mapping Typed models
Manual auth refresh Built-in token lifecycle
Fragile WebSocket code Auto-reconnect, backoff, and 429 handling
Risky order scripts Live trading guardrails and audit logs

Architecture At A Glance

DhanHQ architecture overview

Models own the Ruby API. Resources own HTTP calls. Contracts validate inputs. The transport layer handles auth, retries, rate limiting, and error mapping. WebSockets are a separate subsystem that shares configuration but not the REST stack.

For the full dependency flow and extension pattern, see ARCHITECTURE.md.


✨ Key Features

  • ActiveRecord-style modelsfind, all, where, save, cancel across Orders, Positions, Holdings, Funds, and more
  • Auto token refresh — 401 retry with fresh token via provider callback
  • Thread-safe WebSocket client — Orders, Market Feed, Market Depth with auto-reconnect
  • Exponential backoff + 429 cool-off — no manual rate-limit management
  • Secure logging — automatic token sanitization in all log output
  • Super Orders — entry + stop-loss + target + trailing jump in one request
  • Instrument convenience methods.ltp, .ohlc, .option_chain directly on instruments
  • Order audit logging — every order attempt logs machine, IP, environment, and correlation ID as structured JSON
  • Live trading guard — prevents accidental order placement unless ENV["LIVE_TRADING"]="true"
  • Full REST coverage — Orders, Trades, Forever Orders, Super Orders, Positions, Holdings, Funds, HistoricalData, OptionChain, MarketFeed, EDIS, Kill Switch, P&L Exit, Alert Orders, Margin Calculator
  • P&L Based Exit — automatic position exit on profit/loss thresholds
  • Postback parser — parse Dhan webhook payloads with Postback.parse and status predicates
  • EDIS model — ORM-style T-PIN, form, and status inquiry for delivery instruction slips

Reliability & Safety

  • retry-on-401 with token refresh
  • WebSocket auto-reconnect and backoff
  • 429 rate-limit protection
  • live trading guard via LIVE_TRADING=true
  • structured order audit logs

See ARCHITECTURE.md, docs/TESTING_GUIDE.md, and docs/TROUBLESHOOTING.md for the deeper implementation details.


Installation

# Gemfile (recommended)
gem 'DhanHQ'
bundle install
# or
gem install DhanHQ

Bleeding edge? Use gem 'DhanHQ', git: 'https://github.com/shubhamtaywade82/dhanhq-client.git', branch: 'main' only if you need unreleased features.

bundle update / bundle install warnings — If you see "Local specification for rexml-3.2.8 has different dependencies" or "Unresolved or ambiguous specs during Gem::Specification.reset: psych", the bundle still completes successfully. To clear the rexml warning once, run: gem cleanup rexml. The psych message is a known Bundler quirk and can be ignored.

Gem name vs require path

RubyGems normalizes names, so DhanHQ and dhan_hq refer to the same slot — the published name stays DhanHQ and will never change. The require path has used snake_case since v2.1.5:

# Gemfile              # Ruby file
gem 'DhanHQ'           require 'dhan_hq'

Optional features

The core SDK (require 'dhan_hq') only loads the API client. Technical analysis and the options advisor are opt-in:

require 'dhan_hq/analysis'  # DhanHQ::Analysis::OptionsBuyingAdvisor, MultiTimeframeAnalyzer
require 'dhan_hq/ta'        # TA::TechnicalAnalysis, TA::Fetcher, TA::Candles

Configuration

Static token (simplest)

require 'dhan_hq'
DhanHQ.configure_with_env   # reads DHAN_CLIENT_ID + DHAN_ACCESS_TOKEN from ENV
Variable Purpose
DHAN_CLIENT_ID Your Dhan trading account client ID
DHAN_ACCESS_TOKEN API token from the Dhan console

Dynamic token (production / OAuth)

DhanHQ.configure do |config|
  config.client_id = ENV["DHAN_CLIENT_ID"]
  config.access_token_provider = -> { YourTokenStore.active_token }
  config.on_token_expired = ->(error) { YourTokenStore.refresh! }  # optional
end

When the API returns 401, the client retries once with a fresh token from your provider.

Full details: TOTP flows, partner mode, token endpoint bootstrap, auto-management — see docs/AUTHENTICATION.md.


Order Safety

Live Trading Guard

Order placement (create, slicing) is blocked unless you explicitly enable it:

# Production (Render, VPS, etc.)
LIVE_TRADING=true

# Development / Test (default — orders are blocked)
LIVE_TRADING=false   # or simply omit

Attempting to place an order without LIVE_TRADING=true raises DhanHQ::LiveTradingDisabledError.

Order Audit Logging

Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:

{
  "event": "DHAN_ORDER_ATTEMPT",
  "hostname": "DESKTOP-SHUBHAM",
  "env": "production",
  "ipv4": "122.171.22.40",
  "ipv6": "2401:4900:894c:8448:1da9:27f1:48e7:61be",
  "security_id": "11536",
  "correlation_id": "SCALPER_7af1",
  "timestamp": "2026-03-17T06:45:22Z"
}

This tells you instantly which machine, app, IP, and environment placed the order.

Correlation ID Prefixes

Use per-app prefixes for instant source identification in the Dhan orderbook:

# algo_scalper_api
correlation_id: "SCALPER_#{SecureRandom.hex(4)}"

# algo_trader_api
correlation_id: "TRADER_#{SecureRandom.hex(4)}"

The Dhan orderbook will show SCALPER_7af1 or TRADER_3bc9, making the source obvious.


REST API

Orders — Place, Modify, Cancel

order = DhanHQ::Models::Order.new(
  transaction_type: DhanHQ::Constants::TransactionType::BUY,
  exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_FNO,
  product_type: DhanHQ::Constants::ProductType::MARGIN,
  order_type: DhanHQ::Constants::OrderType::LIMIT,
  validity: DhanHQ::Constants::Validity::DAY,
  security_id:      "43492",
  quantity:         50,
  price:            100.0
)
order.save          # places the order
order.modify(price: 101.5)
order.cancel

Positions, Holdings, Funds

DhanHQ::Models::Position.all
DhanHQ::Models::Holding.all
DhanHQ::Models::Fund.balance

Historical Data

bars = DhanHQ::Models::HistoricalData.intraday(
  security_id:      "13",
  exchange_segment: DhanHQ::Constants::ExchangeSegment::IDX_I,
  instrument: DhanHQ::Constants::InstrumentType::INDEX,
  interval:         "5",
  from_date:        "2025-08-14",
  to_date:          "2025-08-18"
)

Instrument Lookup

nifty = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
nifty.ltp           # last traded price
nifty.ohlc          # OHLC data
nifty.option_chain(expiry: "2025-02-28")
nifty.intraday(from_date: "2025-08-14", to_date: "2025-08-18", interval: "15")

WebSockets

Three real-time feeds, all with auto-reconnect, backoff, 429 cool-off, and thread-safe operation.

Order Updates

DhanHQ::WS::Orders.connect do |order_update|
  puts "#{order_update.order_no}#{order_update.status} (#{order_update.traded_qty}/#{order_update.quantity})"
end

Market Feed (Ticker / Quote / Full)

client = DhanHQ::WS.connect(mode: :ticker) do |tick|
  puts "#{tick[:security_id]} = ₹#{tick[:ltp]}"
end

client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::IDX_I, security_id: "13")   # NIFTY
client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::IDX_I, security_id: "25")   # BANKNIFTY

Market Depth

reliance = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")

DhanHQ::WS::MarketDepth.connect(symbols: [
  { symbol: "RELIANCE", exchange_segment: reliance.exchange_segment, security_id: reliance.security_id }
]) do |depth|
  puts "Best Bid: #{depth[:best_bid]} | Best Ask: #{depth[:best_ask]} | Spread: #{depth[:spread]}"
end

Cleanup

DhanHQ::WS.disconnect_all_local!   # kills all local WS connections

Super Orders

Entry + target + stop-loss + trailing jump in a single request:

DhanHQ::Models::SuperOrder.create(
  transaction_type: DhanHQ::Constants::TransactionType::BUY,
  exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
  product_type: DhanHQ::Constants::ProductType::CNC,
  order_type: DhanHQ::Constants::OrderType::LIMIT,
  security_id:      "11536",
  quantity:         5,
  price:            1500,
  target_price:     1600,
  stop_loss_price:  1400,
  trailing_jump:    10
)

Full API reference (modify, cancel, list, response schemas): docs/SUPER_ORDERS.md


Real-World Example: NIFTY Trend Monitor

require 'dhan_hq'

DhanHQ.configure_with_env

# 1. Check the trend using historical 5-min bars
bars = DhanHQ::Models::HistoricalData.intraday(
  security_id: "13", exchange_segment: DhanHQ::Constants::ExchangeSegment::IDX_I,
  instrument: DhanHQ::Constants::InstrumentType::INDEX, interval: "5",
  from_date: Date.today.to_s, to_date: Date.today.to_s
)

closes = bars.map { |b| b[:close] }
sma_20 = closes.last(20).sum / 20.0
trend  = closes.last > sma_20 ? :bullish : :bearish
puts "NIFTY trend: #{trend} (LTP: #{closes.last}, SMA20: #{sma_20.round(2)})"

# 2. Stream live ticks for real-time monitoring
client = DhanHQ::WS.connect(mode: :quote) do |tick|
  puts "NIFTY ₹#{tick[:ltp]} | Vol: #{tick[:vol]} | #{Time.now.strftime('%H:%M:%S')}"
end
client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::IDX_I, security_id: "13")

# 3. On signal, place a super order with built-in risk management
# DhanHQ::Models::SuperOrder.create(
#   transaction_type: DhanHQ::Constants::TransactionType::BUY, exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_FNO, ...
#   target_price: entry + 50, stop_loss_price: entry - 30, trailing_jump: 5
# )

# 4. Clean shutdown
at_exit { DhanHQ::WS.disconnect_all_local! }
sleep   # keep the script alive

Rails Integration

Need initializers, service objects, ActionCable wiring, and background workers? See the Rails Integration Guide.


Real-World Examples

These scripts are designed around user goals rather than API surfaces:

Example What it covers
examples/basic_trading_bot.rb Trading bot scaffold with live-trading guard
examples/comprehensive_websocket_examples.rb WebSocket mode coverage and timeout handling
examples/options_watchlist.rb Build a live options watchlist with index quotes and option-chain context
examples/market_feed_example.rb Subscribe to major market indices over WebSocket
examples/market_depth_example.rb Market depth streaming example
examples/live_order_updates.rb Track order lifecycle events in real time
examples/order_update_example.rb Single-session order-update flow
examples/portfolio_monitor.rb Snapshot funds, holdings, and positions for a monitoring script
examples/trading_fields_example.rb Dhan order-field mappings and constants example
examples/instrument_finder_test.rb Instrument search/resolution troubleshooting

For search-driven discovery and onboarding content, see:

Use Case Guides


📚 Documentation

Guide What it covers
Architecture Layering, dependency flow, design patterns, extension points
Authentication Token flows, TOTP, OAuth, auto-management
Configuration Reference Full ENV matrix, logging, timeouts, available resources
WebSocket Integration All WS types, architecture, best practices
WebSocket Protocol Packet parsing, request codes, tick schema, exchange enums
Rails WebSocket Guide Rails-specific patterns, ActionCable
Rails Integration Initializers, service objects, workers
Standalone Ruby Guide Scripts, daemons, and long-running Ruby processes
Super Orders API Full REST reference for super orders
API Constants Reference All valid enums, exchange segments, and order parameters
Data API Parameters Historical data, option chain parameters
Testing Guide WebSocket testing, model testing, console helpers
Technical Analysis Indicators, multi-timeframe aggregation
Troubleshooting 429 errors, reconnect, auth issues, debug logging
How To Use Dhan API With Ruby Search-friendly onboarding guide for Ruby users
Build A Trading Bot With Ruby And Dhan End-to-end tutorial framing for strategy builders
Dhan API Ruby Examples Small answer-style snippets for common Ruby + Dhan tasks
Dhan WebSocket Ruby Guide Query-shaped guide for Dhan market data streaming in Ruby
Best Way To Use Dhan API In Ruby Comparison-focused guide for SDK vs raw HTTP
Dhan Ruby Q&A Publish-ready answers for common Dhan + Ruby questions
Release Guide Versioning, publishing, changelog

MCP Server (AI Agent Integration)

DhanHQ includes a built-in Model Context Protocol server that lets AI coding agents (Claude Code, Codex, OpenCode, Cursor) interact with your Dhan account directly from the editor or CLI.

Quick Start

# 1. Configure credentials
export DHAN_CLIENT_ID="your_client_id"
export DHAN_ACCESS_TOKEN="your_access_token"

# 2. Start the server (stdio)
bundle exec dhanhq-mcp

Claude Desktop config (claude_desktop_config.json):

{
  "mcpServers": {
    "dhanhq": {
      "command": "bundle",
      "args": ["exec", "dhanhq-mcp"],
      "env": {
        "DHAN_CLIENT_ID": "your_client_id",
        "DHAN_ACCESS_TOKEN": "your_access_token"
      }
    }
  }
}

MCP Features

Feature Description
Tools 12 primitive tools (profile, funds, holdings, positions, order history, order preview/place/cancel, instruments, market feed) + 11 skill-derived tools (dhan_skill_* — one per builtin strategy in Skills System below)
Resources 6 URI-addressable data endpoints: dhanhq://account/profile, dhanhq://account/funds, dhanhq://account/holdings, dhanhq://account/positions, dhanhq://account/orders, dhanhq://market/capabilities
Prompts 5 pre-built AI prompts: portfolio_summary, market_analysis, risk_report, order_preview, suggest_strategy

Security & Policy

# Read-only mode (no order placement)
DhanHQ::Agent::Policy.read_only

# Scope-based policy from env (DHANHQ_AGENT_SCOPES)
DhanHQ::Agent::Policy.from_env

The policy engine respects DHANHQ_MCP_ENABLE_WRITES and LIVE_TRADING env vars. Write operations are blocked by default — explicit opt-in required.

Skills System

Skills are reusable, composable trading strategies. DhanHQ ships with 11 builtin skills and a registry for discovery and invocation.

Builtin Skills

Skill Type Description
buy_atm_call Single-leg Buy ATM call option
square_off_all Action Square off all open positions
square_off_position Action Square off a specific position
iron_condor Multi-leg Sell OTM put + buy further OTM put + sell OTM call + buy further OTM call
strangle Multi-leg Buy OTM put + buy OTM call
covered_call Multi-leg Buy equity + sell OTM call
bull_put_spread Multi-leg Sell OTM put + buy further OTM put
bear_call_spread Multi-leg Sell OTM call + buy further OTM call
protective_put Multi-leg Buy equity + buy OTM put
straddle Multi-leg Buy ATM call + buy ATM put
market_data_summarizer Read-only Summarize technicals and/or option chain for a symbol

Using Skills

# Register all builtin skills
DhanHQ::Skills::Registry.load_builtins

# Find a skill by name
skill = DhanHQ::Skills::Registry.find("covered_call")

# Invoke — returns an intent hash (trade_type, legs, risk metadata)
result = skill.call(symbol: "RELIANCE", expiry: "2026-06-25")
# => { intent: { trade_type: "COVERED_CALL", legs: [...], total_premium: ..., break_even: ..., note: "..." } }

Skills return intent hashes (not executed trades), keeping a human-in-the-loop safety pattern.

AI Integration

DhanHQ provides prompt helpers and risk reporting for LLM-powered trading agents.

Prompt Helpers

require 'dhan_hq/ai'

# Portfolio summary for AI consumption
DhanHQ::AI::PromptHelpers.portfolio_summary
# => "Portfolio Summary\n━━━━━━━━━━━━━\nFunds: ₹1,00,000.00\n..."

# Risk report
DhanHQ::AI::PromptHelpers.risk_report
# => "🔴 RISK ALERT: 394.6% drawdown..."

Risk Pipeline

# Run pre-trade risk checks
DhanHQ::Risk::Pipeline.run!(
  instrument: instrument,
  args: { quantity: 25, price: 24_500 },
  type: :fno,
  now: Time.now
)

Available checks:

  • TradingPermission — blocks instruments where trading is disabled (buy_sell_indicator != "A")
  • AsmGsm — blocks ASM/GSM restricted instruments
  • ProductSupport — validates bracket/cover order support for the instrument
  • OrderType — restricts to MARKET/LIMIT order types
  • Quantity — max 10 units / ₹1,00,000 notional (an agent-safety limit, not a general trading cap)
  • MarketHours — verifies market is open (9:15 AM–3:30 PM IST)
  • PositionLimits — max 20 concurrent open positions
  • Concentration — max 25% of available balance in a single symbol
  • Options (options only) — index-only, requires stop loss + target + risk-reward
  • MaxLoss (daily) — daily loss limit (default ₹50,000)

Checks raise DhanHQ::RiskViolation with human-readable messages, safe for AI parsing. Wired into every order-placing path via DhanHQ::Concerns::OrderAudit#run_risk_checks! (Orders, SuperOrders, ForeverOrders, AlertOrders, TwapOrders, IcebergOrders, PnL Exit) and into the dhan_place_order MCP tool.

Known Limitations

This gem's core REST/WS client (orders, positions, funds, market data) is mature and already depended on in production by other repos in this workspace. The MCP server, Skills system, and Risk pipeline are newer and have now been verified live end to end — including the full write path — against real Dhan accounts, a real independent MCP client, and a real order-matching engine — see CHANGELOG.md for details:

  • Read path (profile/funds/holdings/positions/orders/instrument lookup/option chains, all 11 skills' intent-building, WebSocket streaming) — live-verified.
  • Write path — dhan_place_order, dhan_cancel_order, dhan_skill_square_off_all, and dhan_skill_square_off_position all verified end-to-end through the MCP-gated path (instrument resolution, full risk pipeline, audit logging, real execution). Dhan's own sandbox never executes real fills (by design, per Dhan's docs), so fills were exercised against simulators/paper_exchange's real matching engine via a throwaway Dhan-API-compatible adapter. Found and fixed a real bug in the process: both square-off skills used p[:net_quantity]-style hash indexing on Position (which has no [] method), silently finding zero positions to close every time — invisible until a real position finally existed to test against.
  • Risk checks that read portfolio state (Concentration, PositionLimits, MaxLoss) have only been observed against zero/low-balance accounts and a single simulated position — not a large multi-symbol portfolio.
  • The sandbox environment's security-ID catalog is disconnected from the production instrument master — resolve IDs from sandbox order history, not Instrument.find, when testing against sandbox.

Best Practices

  • Keep on(:tick) handlers non-blocking — push heavy work to a queue/thread
  • Use mode: :quote for most strategies; :full only if you need depth/OI
  • Don't exceed 100 instruments per subscribe frame (auto-chunked by the client)
  • Call DhanHQ::WS.disconnect_all_local! on shutdown
  • Avoid rapid connect/disconnect loops — the client already backs off on 429
  • Use dynamic token providers in long-running systems instead of hardcoding expiring tokens

Contributing

PRs welcome! Please include tests for new features. See CHANGELOG.md for recent changes.

bundle exec rake          # run tests
bundle exec rubocop       # lint
bin/console               # interactive console

Disclaimer

This gem is an independent, community-maintained project and is not officially affiliated with, endorsed by, or supported by Dhan (Mirae Asset Capital Markets). Trading in financial instruments carries significant risk. Use this SDK at your own risk and always verify order placement in a sandbox environment before going live.

License

MIT