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,76 @@
1
+ # Common Workflows — Agent Playbooks (Ruby SDK)
2
+
3
+ ## Portfolio Rebalance
4
+
5
+ Recommended sequence:
6
+ 1. Fetch holdings and funds.
7
+ 2. Compute target deltas.
8
+ 3. Resolve symbols and quantities.
9
+ 4. Preview proposed orders.
10
+ 5. Confirm with the user.
11
+ 6. Place live orders.
12
+
13
+ ```ruby
14
+ holdings = DhanHQ::Models::Holding.all rescue []
15
+ funds = DhanHQ::Models::Funds.fetch rescue nil
16
+
17
+ if funds
18
+ available_cash = funds.availabel_balance || funds.available_balance || 0.0
19
+ end
20
+ ```
21
+
22
+ ## Delivery Sell With eDIS
23
+
24
+ Use this flow for selling demat holdings:
25
+ 1. Fetch holdings and identify ISIN.
26
+ 2. Generate TPIN: `DhanHQ::Models::EDIS.generate_tpin`
27
+ 3. Open authorization form: `DhanHQ::Models::EDIS.open_browser_for_tpin(isin: "...", qty: 5, exchange: "NSE")`
28
+ 4. Check inquiry: `DhanHQ::Models::EDIS.inquiry(isin: "...")`
29
+ 5. Place the sell order.
30
+
31
+ ```ruby
32
+ # Generate TPIN
33
+ DhanHQ::Models::EDIS.generate_tpin
34
+
35
+ # Open authorization portal
36
+ DhanHQ::Models::EDIS.open_browser_for_tpin(isin: "INE002A01018", qty: 5, exchange: "NSE")
37
+
38
+ # Inquiry
39
+ status = DhanHQ::Models::EDIS.inquiry(isin: "INE002A01018")
40
+ ```
41
+
42
+ ## Single-Leg F&O Execution
43
+
44
+ Recommended sequence:
45
+ 1. Resolve current contract from option chain or security master.
46
+ 2. Resolve lot size.
47
+ 3. Validate quantity.
48
+ 4. Check margin.
49
+ 5. Preview & Confirm.
50
+ 6. Place live order.
51
+
52
+ ```ruby
53
+ require_relative "../scripts/dhan_helpers"
54
+
55
+ chain_df, spot = fetch_chain_df(under_security_id: 13, expiry: "2025-03-27")
56
+ atm = find_atm_row(chain_df, spot)
57
+
58
+ margin = check_margin(
59
+ security_id: atm["ce_security_id"],
60
+ exchange_segment: "NSE_FNO",
61
+ transaction_type: "BUY",
62
+ quantity: 75,
63
+ product_type: "INTRADAY",
64
+ price: atm["ce_ltp"].to_f
65
+ )
66
+ ```
67
+
68
+ ## Daily P&L Summary
69
+
70
+ ```ruby
71
+ require_relative "../scripts/dhan_helpers"
72
+
73
+ holdings = DhanHQ::Models::Holding.all
74
+ positions = DhanHQ::Models::Position.all
75
+ summary = format_pnl_report(holdings, positions)
76
+ ```
@@ -0,0 +1,50 @@
1
+ # Error Codes — Complete Reference (Ruby SDK)
2
+
3
+ In the Ruby SDK, raw API error codes are automatically mapped in the client layer and raised as structured exceptions.
4
+
5
+ ## Exception Mapping
6
+
7
+ The Ruby client maps Dhan error codes to specific error classes under the `DhanHQ` module:
8
+
9
+ | Error Code | Error Class | Meaning |
10
+ |------------|-------------|---------|
11
+ | `DH-901` | `DhanHQ::InvalidAuthenticationError` | Client ID or access token is invalid or expired |
12
+ | `DH-902` | `DhanHQ::InvalidAccessError` | User does not have required Data API or Trading API access |
13
+ | `DH-903` | `DhanHQ::UserAccountError` | Account setup issue or segment activation requirement |
14
+ | `DH-904` | `DhanHQ::RateLimitError` | Rate limit exceeded |
15
+ | `DH-905` | `DhanHQ::InputExceptionError` | Missing or invalid request fields |
16
+ | `DH-906` | `DhanHQ::OrderError` | Order request cannot be processed |
17
+ | `DH-907` | `DhanHQ::DataError` | Data unavailable or parameters invalid |
18
+ | `DH-908` | `DhanHQ::InternalServerError` | Server-side failure |
19
+ | `DH-909` | `DhanHQ::NetworkError` | Backend communication failure |
20
+ | `DH-1111` | `DhanHQ::NoHoldingsError` | No holdings present in the account |
21
+ | `DH-910` / other | `DhanHQ::OtherError` / `DhanHQ::Error` | Other failure reasons |
22
+
23
+ ## Data API Errors
24
+
25
+ | Code | Exception | Meaning |
26
+ |------|-----------|---------|
27
+ | `800` | `DhanHQ::InternalServerError` | Internal Server Error |
28
+ | `804` | `DhanHQ::Error` | Requested number of instruments exceeds limit |
29
+ | `805` | `DhanHQ::RateLimitError` | Too many requests or connections |
30
+ | `806` | `DhanHQ::DataError` | Data APIs not subscribed |
31
+ | `807` | `DhanHQ::TokenExpiredError` | Access token is expired |
32
+ | `808` | `DhanHQ::AuthenticationFailedError` | Authentication failed - client ID or access token invalid |
33
+ | `809` | `DhanHQ::InvalidTokenError` | Access token is invalid |
34
+ | `810` | `DhanHQ::InvalidClientIDError` | Client ID is invalid |
35
+ | `811` | `DhanHQ::InvalidRequestError` | Invalid expiry date |
36
+ | `812` | `DhanHQ::InvalidRequestError` | Invalid date format |
37
+ | `813` | `DhanHQ::InvalidRequestError` | Invalid security ID |
38
+ | `814` | `DhanHQ::InvalidRequestError` | Invalid request |
39
+
40
+ ## User Action Checklist
41
+
42
+ ### Invalid Data Subscription (`806` or `DH-902`)
43
+ If you receive access errors:
44
+ 1. Log in to `web.dhan.co`.
45
+ 2. Go to **My Profile** -> **Access DhanHQ APIs**.
46
+ 3. Verify that the **Data API** plan is active.
47
+ 4. If not active, activate it, generate a fresh access token, and retry.
48
+
49
+ ### Static IP Error (`DH-911` or IP issue)
50
+ If placing or managing orders fails with IP errors, ensure that the server's public IP is whitelisted in your Dhan console.
@@ -0,0 +1,67 @@
1
+ # Funds & Margin — Complete Reference (Ruby SDK)
2
+
3
+ The Ruby SDK exposes first-class models `DhanHQ::Models::Funds` and `DhanHQ::Models::Margin` for funds retrieval and pre-flight margin checks (both single-order and multi-leg).
4
+
5
+ ## Fund Limits
6
+
7
+ Use `DhanHQ::Models::Funds.fetch`:
8
+
9
+ ```ruby
10
+ funds = DhanHQ::Models::Funds.fetch
11
+
12
+ puts "Available Balance: Rs. #{funds.availabel_balance || funds.available_balance}"
13
+ puts "Utilized: Rs. #{funds.utilized_amount}"
14
+ puts "Collateral: Rs. #{funds.collateral_amount}"
15
+ puts "Withdrawable: Rs. #{funds.withdrawable_balance}"
16
+ ```
17
+
18
+ Normalized model attributes:
19
+ - `dhan_client_id`
20
+ - `availabel_balance` (or alias `available_balance`)
21
+ - `sod_limit`
22
+ - `collateral_amount`
23
+ - `receiveable_amount`
24
+ - `utilized_amount`
25
+ - `blocked_payout_amount`
26
+ - `withdrawable_balance`
27
+
28
+ ---
29
+
30
+ ## Margin Calculator — Single Order
31
+
32
+ Use `DhanHQ::Models::Margin.calculate(params)`:
33
+
34
+ ```ruby
35
+ margin = DhanHQ::Models::Margin.calculate(
36
+ security_id: "2885",
37
+ exchange_segment: "NSE_EQ",
38
+ transaction_type: "BUY",
39
+ quantity: 10,
40
+ product_type: "CNC",
41
+ price: 2450.0
42
+ )
43
+
44
+ puts "Total Margin: Rs. #{margin.total_margin}"
45
+ puts "Available Balance: Rs. #{margin.available_balance}"
46
+ puts "Brokerage Charges: Rs. #{margin.brokerage}"
47
+ puts "Leverage Offered: #{margin.leverage}x"
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Multi-Order Margin
53
+
54
+ Unlike the Python SDK, the Ruby SDK has first-class support for multi-leg portfolio margin calculation via `DhanHQ::Models::Margin.calculate_multi(params)`:
55
+
56
+ ```ruby
57
+ margin = DhanHQ::Models::Margin.calculate_multi(
58
+ include_position: true,
59
+ include_orders: true,
60
+ scripts: [
61
+ { exchange_segment: "NSE_EQ", transaction_type: "BUY", quantity: 100, product_type: "CNC", security_id: "1333", price: 1428.0 },
62
+ { exchange_segment: "NSE_EQ", transaction_type: "SELL", quantity: 50, product_type: "INTRADAY", security_id: "11536", price: 3000.0 }
63
+ ]
64
+ )
65
+
66
+ puts "Portfolio Total Margin Required: Rs. #{margin.total_margin}"
67
+ ```
@@ -0,0 +1,85 @@
1
+ # Instruments — Complete Reference (Ruby SDK)
2
+
3
+ Use the security master as the primary source for `security_id`, lot size, expiry, strike, tick size, and display symbol.
4
+
5
+ ## Preferred SDK Entry Point
6
+
7
+ In the Ruby SDK, search and load instruments segment-wise using:
8
+
9
+ ```ruby
10
+ # Retrieve compact list for a single segment (returns Array of Instrument objects)
11
+ instruments = DhanHQ::Models::Instrument.by_segment("NSE_EQ")
12
+ ```
13
+
14
+ Official instrument sources (managed by the SDK internally):
15
+ - Compact CSV: `https://images.dhan.co/api-data/api-scrip-master.csv`
16
+ - Detailed CSV: `https://images.dhan.co/api-data/api-scrip-master-detailed.csv`
17
+
18
+ ---
19
+
20
+ ## Key Columns (Instrument Attributes)
21
+
22
+ | Attribute | Meaning |
23
+ |-----------|---------|
24
+ | `security_id` | Security ID (String) |
25
+ | `exchange` | Exchange ID (`NSE`, `BSE`, `MCX`) |
26
+ | `instrument` | Instrument Type (`EQUITY`, `OPTIDX`, `OPTSTK`, etc.) |
27
+ | `symbol_name` | Exchange trading symbol |
28
+ | `display_name` | Dhan custom symbol |
29
+ | `lot_size` | Lot size (Integer) |
30
+ | `tick_size` | Tick size (Float) |
31
+ | `expiry_date` | Expiry date (String) |
32
+ | `strike_price` | Strike price (Float) |
33
+ | `option_type` | Option Type (`CALL` or `PUT`) |
34
+
35
+ ---
36
+
37
+ ## Recommended Resolution Flow
38
+
39
+ Use the SDK's built-in helper methods on the `Instrument` class:
40
+
41
+ ```ruby
42
+ # Find specific instrument in a segment (exact match)
43
+ inst = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
44
+
45
+ # Search across multiple segments (finds any match)
46
+ inst = DhanHQ::Models::Instrument.find_anywhere("RELIANCE")
47
+
48
+ # Fuzzy search across multiple segments
49
+ results = DhanHQ::Models::Instrument.search("RELIANCE")
50
+ ```
51
+
52
+ Or leverage the helper layer in `scripts/dhan_helpers.rb`:
53
+
54
+ ```ruby
55
+ require_relative "../scripts/dhan_helpers"
56
+
57
+ cash = resolve_symbol("RELIANCE", "NSE_EQ")
58
+ contract = resolve_derivative("NIFTY", strike: 24000, option_type: "CE", expiry: "2025-03-27")
59
+ lot_size = get_lot_size(underlying: "NIFTY")
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Quick-Reference Fallback IDs
65
+
66
+ ### Index Underlyings
67
+
68
+ | Underlying | security_id | Underlying Segment |
69
+ |------------|-------------|-------------------|
70
+ | NIFTY 50 | `13` | `IDX_I` |
71
+ | BANK NIFTY | `25` | `IDX_I` |
72
+ | FINNIFTY | `27` | `IDX_I` |
73
+ | MIDCPNIFTY | `442` | `IDX_I` |
74
+ | SENSEX | `51` | `IDX_I` |
75
+
76
+ ### Common NSE Equities
77
+
78
+ | Symbol | security_id |
79
+ |--------|-------------|
80
+ | RELIANCE | `2885` |
81
+ | HDFCBANK | `1333` |
82
+ | TCS | `11536` |
83
+ | INFY | `1594` |
84
+ | ICICIBANK | `4963` |
85
+ | SBIN | `3045` |
@@ -0,0 +1,83 @@
1
+ # Live Feed — Complete Reference (Ruby SDK)
2
+
3
+ The Ruby SDK provides three distinct WebSocket interfaces under the `DhanHQ::WS` namespace to handle live data streaming.
4
+
5
+ ## 1. Market Feed (`DhanHQ::WS.connect`)
6
+
7
+ Real-time market ticks, last traded prices, quotes, and market depth updates.
8
+
9
+ ### Usage
10
+
11
+ ```ruby
12
+ # Connect to market feed. Modes: :ticker, :quote, :full
13
+ market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
14
+ timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
15
+ puts "Tick: #{tick[:segment]}:#{tick[:security_id]} LTP=#{tick[:ltp]} at #{timestamp}"
16
+ end
17
+
18
+ # Subscribe to segments and security IDs
19
+ market_client.subscribe_one(segment: "NSE_EQ", security_id: "2885")
20
+ market_client.subscribe_one(segment: "NSE_EQ", security_id: "1333")
21
+
22
+ # Stop connection
23
+ sleep(15)
24
+ market_client.stop
25
+ ```
26
+
27
+ ### Modes
28
+ - `:ticker` - LTP (Last Traded Price) only.
29
+ - `:quote` - OHLC + Volume updates.
30
+ - `:full` - Full quote depth (5 levels) and Open Interest (OI) updates.
31
+
32
+ ---
33
+
34
+ ## 2. Order Updates (`DhanHQ::WS::Orders.connect`)
35
+
36
+ Streams real-time updates for placed, modified, executed, or rejected orders.
37
+
38
+ ### Usage
39
+
40
+ ```ruby
41
+ orders_client = DhanHQ::WS::Orders.connect do |update|
42
+ puts "Order Update: #{update.order_no} status=#{update.status}"
43
+ puts " Symbol: #{update.symbol}, Traded: #{update.traded_qty}/#{update.quantity}"
44
+ end
45
+
46
+ # Register event callbacks
47
+ orders_client.on(:update) { |order| puts "📝 Order Modified: #{order.order_no}" }
48
+ orders_client.on(:execution) { |exec| puts "✅ Executed: #{exec[:new_traded_qty]} shares" }
49
+ orders_client.on(:order_rejected) { |order| puts "❌ Rejected: #{order.order_no}" }
50
+
51
+ sleep(15)
52
+ orders_client.stop
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 3. Market Depth (`DhanHQ::WS::MarketDepth.connect`)
58
+
59
+ Streams order book depth (bid/ask levels). Supports 20-level depth.
60
+
61
+ ### Usage
62
+
63
+ ```ruby
64
+ symbols = [
65
+ { symbol: "RELIANCE", exchange_segment: "NSE_EQ", security_id: "2885" },
66
+ { symbol: "TCS", exchange_segment: "NSE_EQ", security_id: "11536" }
67
+ ]
68
+
69
+ depth_client = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth|
70
+ puts "Symbol: #{depth[:symbol]} Spread: #{depth[:spread]}"
71
+ puts " Best Bid: #{depth[:best_bid]} | Best Ask: #{depth[:best_ask]}"
72
+ end
73
+
74
+ sleep(15)
75
+ depth_client.stop
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Connection Limits & Cleanup
81
+
82
+ - Dhan allows up to **5 concurrent WebSocket connections** per client account.
83
+ - Always call `client.stop` or `DhanHQ::WS.disconnect_all_local!` to prevent socket leaks and rate-limit issues (`429 Too Many Requests`).
@@ -0,0 +1,119 @@
1
+ # Market Data — Complete Reference (Ruby SDK)
2
+
3
+ Timestamps returned by the `HistoricalData` model are automatically normalized into Ruby `Time` objects.
4
+
5
+ ## Historical Daily Data
6
+
7
+ Use `DhanHQ::Models::HistoricalData.daily(params)`:
8
+
9
+ ```ruby
10
+ candles = DhanHQ::Models::HistoricalData.daily(
11
+ security_id: "2885",
12
+ exchange_segment: "NSE_EQ",
13
+ instrument: "EQUITY",
14
+ from_date: "2024-01-01",
15
+ to_date: "2024-12-31",
16
+ expiry_code: 0, # Optional: 0 for current, 1 for next, 2 for far
17
+ oi: false # Optional: true to include open interest
18
+ )
19
+
20
+ first_candle = candles.first
21
+ puts "Date: #{first_candle[:timestamp]}, Close: ₹#{first_candle[:close]}"
22
+ ```
23
+
24
+ Each candle in the returned array is a Hash containing:
25
+ - `:timestamp` (Ruby `Time` object)
26
+ - `:open` (Float)
27
+ - `:high` (Float)
28
+ - `:low` (Float)
29
+ - `:close` (Float)
30
+ - `:volume` (Integer)
31
+ - `:open_interest` (Float, only if `oi: true` was requested)
32
+
33
+ ## Intraday Minute Data
34
+
35
+ Use `DhanHQ::Models::HistoricalData.intraday(params)`:
36
+
37
+ ```ruby
38
+ candles = DhanHQ::Models::HistoricalData.intraday(
39
+ security_id: "2885",
40
+ exchange_segment: "NSE_EQ",
41
+ instrument: "EQUITY",
42
+ interval: "15", # Supported: "1", "5", "15", "25", "60"
43
+ from_date: "2024-09-11 09:30:00",
44
+ to_date: "2024-09-15 13:00:00",
45
+ oi: false
46
+ )
47
+ ```
48
+
49
+ - Max 90 days of data can be polled in a single request.
50
+ - Returns a normalized array of candle hashes.
51
+
52
+ ---
53
+
54
+ ## Market Quote Snapshots
55
+
56
+ REST quote snapshots are accessed via the `DhanHQ::Models::MarketFeed` model.
57
+
58
+ ### Ticker Data (LTP only)
59
+
60
+ ```ruby
61
+ response = DhanHQ::Models::MarketFeed.ltp(
62
+ "NSE_EQ" => [2885, 1333],
63
+ "NSE_FNO" => [49081]
64
+ )
65
+
66
+ ltp = response[:data]["NSE_EQ"]["2885"][:last_price]
67
+ ```
68
+
69
+ ### OHLC Data
70
+
71
+ ```ruby
72
+ response = DhanHQ::Models::MarketFeed.ohlc(
73
+ "NSE_EQ" => [2885]
74
+ )
75
+
76
+ ohlc = response[:data]["NSE_EQ"]["2885"][:ohlc]
77
+ ```
78
+
79
+ ### Quote Data (Full Quote Depth & Analytics)
80
+
81
+ ```ruby
82
+ response = DhanHQ::Models::MarketFeed.quote(
83
+ "NSE_FNO" => [49081]
84
+ )
85
+
86
+ quote = response[:data]["NSE_FNO"]["49081"]
87
+ puts "LTP: #{quote[:last_price]}, OI: #{quote[:oi]}, Vol: #{quote[:volume]}"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Expired Options Data
93
+
94
+ Use `DhanHQ::Models::ExpiredOptionsData.fetch(params)` (or direct resource access):
95
+
96
+ ```ruby
97
+ response = DhanHQ::Models::ExpiredOptionsData.fetch(
98
+ underlying_scrip: 13,
99
+ exchange_segment: "NSE_FNO",
100
+ expiry_flag: "MONTH",
101
+ expiry_code: 1,
102
+ strike: "ATM",
103
+ option_type: "CALL",
104
+ required_data: ["open", "high", "low", "close", "volume", "oi", "spot"],
105
+ from_date: "2021-08-01",
106
+ to_date: "2021-08-31",
107
+ interval: "1"
108
+ )
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Timestamp Conversion
114
+
115
+ If using raw API responses where timestamps are UNIX epochs, convert them to Ruby Time:
116
+
117
+ ```ruby
118
+ time = Time.at(epoch_timestamp)
119
+ ```
@@ -0,0 +1,71 @@
1
+ # Option Chain — Complete Reference (Ruby SDK)
2
+
3
+ For analysis code, use the helper layer `fetch_chain_df` from `scripts/dhan_helpers.rb`.
4
+
5
+ ## Expiry List
6
+
7
+ Use `DhanHQ::Models::OptionChain.fetch_expiry_list(params)`:
8
+
9
+ ```ruby
10
+ expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
11
+ underlying_scrip: 13,
12
+ underlying_seg: "IDX_I"
13
+ )
14
+ ```
15
+
16
+ ## Option Chain
17
+
18
+ Use `DhanHQ::Models::OptionChain.fetch(params)`:
19
+
20
+ ```ruby
21
+ chain = DhanHQ::Models::OptionChain.fetch(
22
+ underlying_scrip: 13,
23
+ underlying_seg: "IDX_I",
24
+ expiry: "2025-03-27"
25
+ )
26
+
27
+ # Underlying LTP
28
+ spot = chain[:last_price]
29
+
30
+ # Strikes sorted array
31
+ chain[:strikes].each do |strike_data|
32
+ puts "Strike: #{strike_data[:strike]}"
33
+ puts "Call LTP: #{strike_data[:call][:last_price]}"
34
+ puts "Put Delta: #{strike_data[:put][:greeks][:delta]}"
35
+ end
36
+ ```
37
+
38
+ ### Rate Limits
39
+ - Calls are limited to **1 request every 3 seconds**. The SDK's internal rate limiter handles this.
40
+
41
+ ---
42
+
43
+ ## Normalized Helper Layer
44
+
45
+ ```ruby
46
+ require_relative "../scripts/dhan_helpers"
47
+
48
+ chain_rows, spot = fetch_chain_df(
49
+ under_security_id: 13,
50
+ expiry: "2025-03-27",
51
+ under_exchange_segment: "IDX_I"
52
+ )
53
+
54
+ atm = find_atm_row(chain_rows, spot)
55
+ puts "Spot: #{spot}, ATM Strike: #{atm['strike']}, Call LTP: #{atm['ce_ltp']}"
56
+ ```
57
+
58
+ Normalized columns returned by `fetch_chain_df`:
59
+ - `strike`
60
+ - `ce_security_id`, `pe_security_id`
61
+ - `ce_ltp`, `pe_ltp`
62
+ - `ce_oi`, `pe_oi`
63
+ - `ce_oi_change`, `pe_oi_change`
64
+ - `ce_volume`, `pe_volume`
65
+ - `ce_iv`, `pe_iv`
66
+ - `ce_bid_price`, `pe_bid_price`
67
+ - `ce_ask_price`, `pe_ask_price`
68
+ - `ce_delta`, `pe_delta`
69
+ - `ce_gamma`, `pe_gamma`
70
+ - `ce_theta`, `pe_theta`
71
+ - `ce_vega`, `pe_vega`
@@ -0,0 +1,76 @@
1
+ # Options Analysis Patterns (Ruby SDK)
2
+
3
+ Use the normalized helper output from `scripts/dhan_helpers.rb` for option chain analysis:
4
+
5
+ ```ruby
6
+ require_relative "../scripts/dhan_helpers"
7
+
8
+ chain_rows, spot = fetch_chain_df(
9
+ under_security_id: 13,
10
+ expiry: "2025-03-27",
11
+ under_exchange_segment: "IDX_I"
12
+ )
13
+
14
+ atm = find_atm_row(chain_rows, spot)
15
+ ```
16
+
17
+ ## Put-Call Ratio (PCR)
18
+
19
+ ```ruby
20
+ total_ce_oi = chain_rows.sum { |r| r["ce_oi"].to_f }
21
+ total_pe_oi = chain_rows.sum { |r| r["pe_oi"].to_f }
22
+ pcr = total_ce_oi > 0 ? (total_pe_oi / total_ce_oi) : 0.0
23
+ puts "PCR: #{'%.2f' % pcr}"
24
+ ```
25
+
26
+ ## OI Support / Resistance
27
+
28
+ Find strikes with the highest open interest for resistance (CE) and support (PE):
29
+
30
+ ```ruby
31
+ # Top 3 resistance walls (highest Call OI)
32
+ ce_walls = chain_rows.sort_by { |r| -(r["ce_oi"] || 0) }.first(3)
33
+
34
+ # Top 3 support walls (highest Put OI)
35
+ pe_walls = chain_rows.sort_by { |r| -(r["pe_oi"] || 0) }.first(3)
36
+ ```
37
+
38
+ ## IV Skew
39
+
40
+ ```ruby
41
+ otm_puts = chain_rows.select { |r| r["strike"] < spot }.sort_by { |r| -r["strike"] }.first(3)
42
+ otm_calls = chain_rows.select { |r| r["strike"] > spot }.sort_by { |r| r["strike"] }.first(3)
43
+
44
+ put_iv_avg = otm_puts.sum { |r| r["pe_iv"].to_f } / otm_puts.size.to_f
45
+ call_iv_avg = otm_calls.sum { |r| r["ce_iv"].to_f } / otm_calls.size.to_f
46
+ skew = put_iv_avg - call_iv_avg
47
+ ```
48
+
49
+ ## Max Pain
50
+
51
+ Calculate the option strike price where option buyers would experience the maximum loss:
52
+
53
+ ```ruby
54
+ def calculate_max_pain(chain_rows)
55
+ strikes = chain_rows.map { |r| r["strike"] }
56
+ pain = {}
57
+
58
+ strikes.each do |test_price|
59
+ total = 0.0
60
+ chain_rows.each do |row|
61
+ strike = row["strike"]
62
+ ce_oi = row["ce_oi"].to_f
63
+ pe_oi = row["pe_oi"].to_f
64
+
65
+ total += [test_price - strike, 0.0].max * ce_oi
66
+ total += [strike - test_price, 0.0].max * pe_oi
67
+ end
68
+ pain[test_price] = total
69
+ end
70
+
71
+ pain.min_by { |_strike, total_pain| total_pain }&.first
72
+ end
73
+
74
+ max_pain_strike = calculate_max_pain(chain_rows)
75
+ puts "Max Pain Strike: #{max_pain_strike}"
76
+ ```