DhanHQ 3.2.0 → 3.2.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +23 -0
  3. data/CHANGELOG.md +26 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
  6. data/lib/DhanHQ/client.rb +53 -34
  7. data/lib/DhanHQ/models/alert_order.rb +1 -1
  8. data/lib/DhanHQ/risk/pipeline.rb +0 -2
  9. data/lib/DhanHQ/version.rb +1 -1
  10. data/lib/dhan_hq.rb +4 -0
  11. data/skills/dhanhq-ruby/SKILL.md +208 -0
  12. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  13. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  14. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  15. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  16. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  17. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  18. data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
  19. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  20. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  21. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  22. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  23. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  24. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  25. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  26. data/skills/dhanhq-ruby/references/funds.md +67 -0
  27. data/skills/dhanhq-ruby/references/instruments.md +91 -0
  28. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  29. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  30. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  31. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  32. data/skills/dhanhq-ruby/references/orders.md +203 -0
  33. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  34. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  35. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  36. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  37. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  38. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  39. metadata +32 -2
@@ -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
+ ```
@@ -0,0 +1,203 @@
1
+ # Orders — Complete Reference (Ruby SDK)
2
+
3
+ Critical API rules:
4
+ - Order placement, modification, cancellation, super orders, and forever orders require static IP whitelisting.
5
+ - Dhan's current order docs say API market orders are converted to limit orders with MPP.
6
+ - **`ENV["LIVE_TRADING"]="true"` is required for `place`/`create`/`modify`/`cancel` to actually submit anything** — the gem raises `DhanHQ::LiveTradingDisabledError` otherwise, as a safety gate against accidental order placement from a dev machine.
7
+ - **`correlation_id` must be 25 characters or fewer.** Dhan's real API rejects the entire order with a generic `DH-905` error if exceeded — it doesn't say which field was wrong.
8
+
9
+ ## Regular Orders
10
+
11
+ ### Place Order
12
+
13
+ In the Ruby SDK, prefer using the model class `DhanHQ::Models::Order.place(params)`:
14
+
15
+ ```ruby
16
+ order = DhanHQ::Models::Order.place(
17
+ security_id: "2885",
18
+ exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
19
+ transaction_type: DhanHQ::Constants::TransactionType::BUY,
20
+ quantity: 10,
21
+ order_type: DhanHQ::Constants::OrderType::LIMIT,
22
+ product_type: DhanHQ::Constants::ProductType::CNC,
23
+ price: 2450.0,
24
+ validity: DhanHQ::Constants::Validity::DAY,
25
+ correlation_id: "rebalance_001"
26
+ )
27
+
28
+ if order
29
+ puts "Placed Order ID: #{order.order_id}, Status: #{order.order_status}"
30
+ end
31
+ ```
32
+
33
+ Alternatively, you can use the ActiveRecord-style `new` and `save` flow:
34
+
35
+ ```ruby
36
+ order = DhanHQ::Models::Order.new(
37
+ security_id: "2885",
38
+ exchange_segment: "NSE_EQ",
39
+ transaction_type: "BUY",
40
+ quantity: 10,
41
+ order_type: "LIMIT",
42
+ product_type: "CNC",
43
+ price: 2450.0,
44
+ validity: "DAY"
45
+ )
46
+ order.save # Places the order via API
47
+ ```
48
+
49
+ ### Slice Order
50
+
51
+ If placing a large quantity that exceeds exchange freeze limits, the SDK handles slicing automatically when using the slice API:
52
+
53
+ ```ruby
54
+ order.slice_order(
55
+ slice_quantity: 1000
56
+ )
57
+ ```
58
+
59
+ ### Modify Order
60
+
61
+ Modify a pending order directly on the model instance:
62
+
63
+ ```ruby
64
+ order = DhanHQ::Models::Order.find("112111182198")
65
+ if order.pending?
66
+ order.modify(
67
+ price: 2455.0,
68
+ quantity: 10,
69
+ validity: "DAY"
70
+ )
71
+ end
72
+ ```
73
+
74
+ The modify request expects the full placed quantity, not the pending quantity.
75
+
76
+ ### Cancel Order
77
+
78
+ Cancel a pending order directly on the model instance:
79
+
80
+ ```ruby
81
+ order = DhanHQ::Models::Order.find("112111182198")
82
+ order.cancel # Returns true on success
83
+ ```
84
+
85
+ ### Order Retrieval
86
+
87
+ ```ruby
88
+ # Fetch all orders for today
89
+ orders = DhanHQ::Models::Order.all
90
+
91
+ # Find order by ID
92
+ order = DhanHQ::Models::Order.find("112111182198")
93
+
94
+ # Find order by correlation ID
95
+ order = DhanHQ::Models::Order.find_by_correlation("rebalance_001")
96
+
97
+ # Fetch today's trades
98
+ trades = DhanHQ::Models::Trade.today
99
+
100
+ # Find trades by order ID
101
+ trade = DhanHQ::Models::Trade.find_by_order_id("112111182198")
102
+
103
+ # Fetch trade history
104
+ history = DhanHQ::Models::Trade.history(
105
+ from_date: "2025-01-01",
106
+ to_date: "2025-01-31",
107
+ page: 0
108
+ )
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Super Orders (Bracket/Cover Orders)
114
+
115
+ ### Place Super Order
116
+
117
+ Use the `DhanHQ::Models::SuperOrder.create` method:
118
+
119
+ ```ruby
120
+ super_order = DhanHQ::Models::SuperOrder.create(
121
+ security_id: "2885",
122
+ exchange_segment: "NSE_EQ",
123
+ transaction_type: "BUY",
124
+ quantity: 1,
125
+ order_type: "LIMIT",
126
+ product_type: "INTRADAY",
127
+ price: 2450.0,
128
+ target_price: 2500.0,
129
+ stop_loss_price: 2420.0,
130
+ trailing_jump: 10.0
131
+ )
132
+
133
+ puts "Placed Super Order ID: #{super_order.order_id}"
134
+ ```
135
+
136
+ ### Modify Super Order
137
+
138
+ ```ruby
139
+ super_order.modify(
140
+ leg_name: "ENTRY_LEG",
141
+ price: 2455.0,
142
+ quantity: 1,
143
+ target_price: 2510.0,
144
+ stop_loss_price: 2425.0,
145
+ trailing_jump: 10.0
146
+ )
147
+ ```
148
+
149
+ - `ENTRY_LEG` can modify the whole structure while the entry order is `PENDING` or `PART_TRADED`.
150
+ - After entry is `TRADED`, only price and trailing jump of `TARGET_LEG` and `STOP_LOSS_LEG` can be modified.
151
+
152
+ ### Cancel Super Order
153
+
154
+ ```ruby
155
+ super_order.cancel("ENTRY_LEG") # Cancels all legs
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Forever Orders (GTT Orders)
161
+
162
+ ### Place Forever Order
163
+
164
+ Use the `DhanHQ::Models::ForeverOrder.create` method:
165
+
166
+ ```ruby
167
+ # Single Trigger
168
+ gtt_order = DhanHQ::Models::ForeverOrder.create(
169
+ security_id: "2885",
170
+ exchange_segment: "NSE_EQ",
171
+ transaction_type: "BUY",
172
+ product_type: "CNC",
173
+ order_type: "LIMIT",
174
+ quantity: 5,
175
+ price: 2300.0,
176
+ trigger_price: 2305.0,
177
+ order_flag: "SINGLE",
178
+ validity: "DAY"
179
+ )
180
+
181
+ # OCO (One Cancels Other) target + stop loss
182
+ oco_order = DhanHQ::Models::ForeverOrder.create(
183
+ security_id: "2885",
184
+ exchange_segment: "NSE_EQ",
185
+ transaction_type: "SELL",
186
+ product_type: "CNC",
187
+ order_type: "LIMIT",
188
+ quantity: 5,
189
+ price: 2700.00, # Target price (price of first leg)
190
+ trigger_price: 2695.00, # Target trigger price (trigger of first leg)
191
+ price1: 2200.00, # Stop loss price (price of second leg)
192
+ trigger_price1: 2205.00, # Stop loss trigger price (trigger of second leg)
193
+ quantity1: 5, # Stop loss quantity (quantity of second leg)
194
+ order_flag: "OCO",
195
+ validity: "DAY"
196
+ )
197
+ ```
198
+
199
+ ### Cancel Forever Order
200
+
201
+ ```ruby
202
+ gtt_order.cancel # Returns true on success
203
+ ```
@@ -0,0 +1,93 @@
1
+ # Portfolio And Positions — Complete Reference (Ruby SDK)
2
+
3
+ ## Holdings
4
+
5
+ Use `DhanHQ::Models::Holding.all`:
6
+
7
+ ```ruby
8
+ holdings = DhanHQ::Models::Holding.all
9
+
10
+ holdings.each do |holding|
11
+ puts "#{holding.trading_symbol} available=#{holding.available_qty}"
12
+ end
13
+ ```
14
+
15
+ Useful holding fields:
16
+ - `exchange`
17
+ - `trading_symbol`
18
+ - `security_id`
19
+ - `isin`
20
+ - `total_qty`
21
+ - `dp_qty`
22
+ - `t1_qty`
23
+ - `available_qty`
24
+ - `collateral_qty`
25
+ - `avg_cost_price`
26
+
27
+ ---
28
+
29
+ ## Positions
30
+
31
+ Use `DhanHQ::Models::Position.all`:
32
+
33
+ ```ruby
34
+ positions = DhanHQ::Models::Position.all
35
+ open_positions = positions.select { |p| p.net_qty.to_i != 0 }
36
+ ```
37
+
38
+ Useful position fields:
39
+ - `trading_symbol`
40
+ - `security_id`
41
+ - `position_type` # "LONG" or "SHORT"
42
+ - `exchange_segment`
43
+ - `product_type`
44
+ - `buy_avg`
45
+ - `buy_qty`
46
+ - `sell_avg`
47
+ - `sell_qty`
48
+ - `net_qty`
49
+ - `realized_profit`
50
+ - `unrealized_profit`
51
+
52
+ ---
53
+
54
+ ## Convert Position
55
+
56
+ Convert an open position (e.g. from Intraday to CNC/Carry Forward):
57
+
58
+ ```ruby
59
+ # In the Ruby SDK, call convert directly on a Position model instance
60
+ position = DhanHQ::Models::Position.all.first
61
+ position.convert(
62
+ from_product_type: "INTRADAY",
63
+ to_product_type: "CNC",
64
+ position_type: "LONG",
65
+ convert_qty: 1
66
+ )
67
+ ```
68
+
69
+ ---
70
+
71
+ ## eDIS Authorization
72
+
73
+ For selling delivery holdings, authorization is handled via `DhanHQ::Models::EDIS`:
74
+
75
+ ### Step 1: Generate TPIN
76
+ ```ruby
77
+ DhanHQ::Models::EDIS.generate_tpin
78
+ ```
79
+
80
+ ### Step 2: Open Browser Authorization
81
+ ```ruby
82
+ DhanHQ::Models::EDIS.open_browser_for_tpin(
83
+ isin: "INE002A01018",
84
+ qty: 5,
85
+ exchange: "NSE"
86
+ )
87
+ ```
88
+
89
+ ### Step 3: Inquiry eDIS Approval
90
+ ```ruby
91
+ inquiry = DhanHQ::Models::EDIS.inquiry(isin: "INE002A01018")
92
+ puts "Approved Qty: #{inquiry.aprvd_qty}, Status: #{inquiry.status}"
93
+ ```
@@ -0,0 +1,62 @@
1
+ # ScanX — Fundamental and Technical Data
2
+
3
+ Use ScanX when Dhan APIs do not cover the needed data. Dhan provides execution, quotes, OHLC, option chain, and portfolio. ScanX provides fundamentals, technical indicators, shareholding, and screeners.
4
+
5
+ ## Capability Gap
6
+
7
+ | Data needed | Use |
8
+ |------------|-----|
9
+ | PE ratio, EPS, Book Value, PB Ratio | ScanX |
10
+ | Revenue, Net Profit, EBITDA | ScanX |
11
+ | Debt-to-equity, Return on Equity | ScanX |
12
+ | RSI(14), MACD(12,26), ADX(14), ATR(14) | ScanX |
13
+ | Promoter %, FII %, DII %, Public % | ScanX |
14
+ | Quarterly results history (2015–present) | ScanX |
15
+ | Balance Sheet, Cash Flows | ScanX |
16
+ | Stock screeners (fundamental/technical) | ScanX |
17
+ | Live quotes, OHLC, option chain | Dhan |
18
+ | Order execution, portfolio | Dhan |
19
+
20
+ ---
21
+
22
+ ## Company Page URL Pattern
23
+
24
+ `https://scanx.trade/company/{slug}`
25
+
26
+ Slug rules:
27
+ - Lowercase the full registered company name.
28
+ - Replace spaces with hyphens.
29
+ - Include "ltd" if part of the official name.
30
+
31
+ ---
32
+
33
+ ## Combined Workflow: Analyze on ScanX → Execute on Dhan
34
+
35
+ ```ruby
36
+ # Step 1: fetch ScanX page for fundamentals/technicals
37
+ # -> https://scanx.trade/company/reliance-industries-ltd
38
+ # -> Extract metrics: PE, RSI, etc.
39
+
40
+ # Step 2: resolve security_id from Dhan security master
41
+ require_relative "../scripts/dhan_helpers"
42
+ get_client
43
+
44
+ row = resolve_symbol("RELIANCE", "NSE_EQ")
45
+ security_id = row["security_id"]
46
+
47
+ # Step 3: Get live quotes from Dhan
48
+ quote_resp = DhanHQ::Models::MarketFeed.ltp("NSE_EQ" => [security_id.to_i])
49
+ ltp = quote_resp[:data]["NSE_EQ"][security_id.to_s][:last_price].to_f
50
+
51
+ # Step 4: Place order via Dhan
52
+ order = DhanHQ::Models::Order.place(
53
+ security_id: security_id,
54
+ exchange_segment: "NSE_EQ",
55
+ transaction_type: "BUY",
56
+ quantity: 1,
57
+ order_type: "LIMIT",
58
+ product_type: "CNC",
59
+ price: ltp,
60
+ validity: "DAY"
61
+ )
62
+ ```