DhanHQ 3.2.0 → 3.3.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +23 -0
  3. data/CHANGELOG.md +74 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/GUIDE.md +16 -0
  6. data/README.md +54 -1
  7. data/docs/CONFIGURATION.md +38 -14
  8. data/docs/RELEASE_GUIDE.md +1 -1
  9. data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
  10. data/lib/DhanHQ/client.rb +53 -34
  11. data/lib/DhanHQ/concerns/bang_writes.rb +69 -0
  12. data/lib/DhanHQ/concerns/tracked_writes.rb +56 -0
  13. data/lib/DhanHQ/configuration.rb +20 -1
  14. data/lib/DhanHQ/core/base_model.rb +6 -13
  15. data/lib/DhanHQ/deprecation.rb +62 -0
  16. data/lib/DhanHQ/models/alert_order.rb +8 -1
  17. data/lib/DhanHQ/models/forever_order.rb +9 -0
  18. data/lib/DhanHQ/models/global_stocks/order.rb +9 -0
  19. data/lib/DhanHQ/models/iceberg_order.rb +9 -0
  20. data/lib/DhanHQ/models/multi_order.rb +7 -0
  21. data/lib/DhanHQ/models/order.rb +28 -0
  22. data/lib/DhanHQ/models/pnl_exit.rb +7 -0
  23. data/lib/DhanHQ/models/super_order.rb +9 -0
  24. data/lib/DhanHQ/models/twap_order.rb +9 -0
  25. data/lib/DhanHQ/risk/pipeline.rb +0 -2
  26. data/lib/DhanHQ/version.rb +1 -1
  27. data/lib/DhanHQ/write_result.rb +163 -0
  28. data/lib/dhan_hq.rb +9 -0
  29. data/skills/dhanhq-ruby/SKILL.md +208 -0
  30. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  31. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  32. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  33. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  34. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  35. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  36. data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
  37. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  38. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  39. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  40. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  41. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  42. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  43. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  44. data/skills/dhanhq-ruby/references/funds.md +67 -0
  45. data/skills/dhanhq-ruby/references/instruments.md +91 -0
  46. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  47. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  48. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  49. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  50. data/skills/dhanhq-ruby/references/orders.md +203 -0
  51. data/skills/dhanhq-ruby/references/portfolio.md +100 -0
  52. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  53. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  54. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  55. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  56. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  57. metadata +36 -2
@@ -0,0 +1,208 @@
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)
21
+
22
+ Use this skill when an agent needs to write, review, or operate Ruby code using the `DhanHQ` gem.
23
+
24
+ ## Safety Rules — Always Enforce
25
+
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.
34
+
35
+ ## Setup
36
+
37
+ Require the core library and configure using environment variables or a configuration block:
38
+
39
+ ```ruby
40
+ require "dhan_hq"
41
+
42
+ # Configures from environment variables: DHAN_CLIENT_ID, DHAN_ACCESS_TOKEN
43
+ DhanHQ.configure_with_env
44
+ ```
45
+
46
+ Or configure explicitly:
47
+
48
+ ```ruby
49
+ DhanHQ.configure do |config|
50
+ config.client_id = "YOUR_CLIENT_ID"
51
+ config.access_token = "YOUR_ACCESS_TOKEN"
52
+ end
53
+ ```
54
+
55
+ If generating scripts inside this repo, prefer using the helper script:
56
+
57
+ ```ruby
58
+ require_relative "scripts/dhan_helpers"
59
+ get_client
60
+ ```
61
+
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 by symbol | `DhanHQ::Models::Instrument.find(exchange_segment, symbol, options)` |
108
+ | Find specific instrument by security ID | `DhanHQ::Models::Instrument.find_by_security_id(exchange_segment, security_id)` |
109
+ | Find instrument anywhere | `DhanHQ::Models::Instrument.find_anywhere(symbol, options)` |
110
+ | Super orders | `DhanHQ::Models::SuperOrder.create(params)` |
111
+ | Forever orders | `DhanHQ::Models::ForeverOrder.create(params)` |
112
+ | Live market feed | `DhanHQ::WS.connect(mode: :ticker) { |tick| ... }` |
113
+ | Live order updates | `DhanHQ::WS::Orders.connect { |update| ... }` |
114
+ | Live market depth | `DhanHQ::WS::MarketDepth.connect(symbols: [{...}]) { |depth| ... }` |
115
+
116
+ ## High-Value Gotchas
117
+
118
+ - **Return values are model instances:** Most class methods return `DhanHQ::Models` objects rather than raw HTTP hashes.
119
+ - **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.
120
+ - **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`).
121
+ - **Timestamps:** Timestamps returned by `HistoricalData` are automatically normalized into Ruby `Time` objects.
122
+ - **WebSocket connection management:** Use sequential connections or single connection pools to avoid 429 rate limiting. Dhan allows up to 5 concurrent WebSocket connections.
123
+
124
+ ## Core Patterns
125
+
126
+ ### 1. Check account access before data calls
127
+
128
+ ```ruby
129
+ funds = DhanHQ::Models::Funds.fetch
130
+ puts "Available Balance: Rs. #{funds.availabel_balance}"
131
+ ```
132
+
133
+ ### 2. Fetch historical data
134
+
135
+ ```ruby
136
+ candles = DhanHQ::Models::HistoricalData.daily(
137
+ security_id: "2885",
138
+ exchange_segment: "NSE_EQ",
139
+ instrument: "EQUITY",
140
+ from_date: "2024-01-01",
141
+ to_date: "2024-12-31"
142
+ )
143
+
144
+ candles.each do |candle|
145
+ puts "Date: #{candle[:timestamp].to_date}, Close: #{candle[:close]}"
146
+ end
147
+ ```
148
+
149
+ ### 3. Option-chain data
150
+
151
+ ```ruby
152
+ chain = DhanHQ::Models::OptionChain.fetch(
153
+ underlying_scrip: 13,
154
+ underlying_seg: "IDX_I",
155
+ expiry: "2025-03-27"
156
+ )
157
+
158
+ puts "Underlying spot: #{chain[:last_price]}"
159
+ chain[:strikes].each do |strike_data|
160
+ puts "Strike: #{strike_data[:strike]}, Call LTP: #{strike_data[:call][:last_price]}"
161
+ end
162
+ ```
163
+
164
+ ### 4. Margin check before live order placement
165
+
166
+ ```ruby
167
+ margin = DhanHQ::Models::Margin.calculate(
168
+ security_id: "2885",
169
+ exchange_segment: "NSE_EQ",
170
+ transaction_type: "BUY",
171
+ quantity: 10,
172
+ product_type: "CNC",
173
+ price: 2450.0
174
+ )
175
+
176
+ puts "Sufficient balance? #{margin.available_balance >= margin.total_margin}"
177
+ ```
178
+
179
+ ### 5. Live market feed
180
+
181
+ ```ruby
182
+ market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
183
+ puts "Market Tick: #{tick[:security_id]} = #{tick[:ltp]}"
184
+ end
185
+
186
+ market_client.subscribe_one(segment: "NSE_EQ", security_id: "2885")
187
+ sleep(10)
188
+ market_client.stop
189
+ ```
190
+
191
+ ## Reference Files
192
+
193
+ Refer to the documents in the references directory for focused workflows:
194
+
195
+ | Need | File |
196
+ |------|------|
197
+ | Orders, super orders, forever orders | [references/orders.md](references/orders.md) |
198
+ | Holdings, positions, eDIS | [references/portfolio.md](references/portfolio.md) |
199
+ | Daily/minute history, quotes, expired options | [references/market-data.md](references/market-data.md) |
200
+ | Option-chain usage | [references/option-chain.md](references/option-chain.md) |
201
+ | Fund limits and margin checks | [references/funds.md](references/funds.md) |
202
+ | Live feeds and depth | [references/live-feed.md](references/live-feed.md) |
203
+ | Error handling | [references/error-codes.md](references/error-codes.md) |
204
+ | Instrument resolution | [references/instruments.md](references/instruments.md) |
205
+ | Multi-step execution patterns | [references/common-workflows.md](references/common-workflows.md) |
206
+ | Options analytics | [references/options-analysis-patterns.md](references/options-analysis-patterns.md) |
207
+ | Backtesting patterns | [references/backtesting-with-dhan.md](references/backtesting-with-dhan.md) |
208
+ | 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
@@ -0,0 +1,137 @@
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
+ chain_df, spot = fetch_chain_df(under_security_id: 13, expiry: nearest_expiry)
19
+ puts "Nifty Spot: #{spot}, Expiry: #{nearest_expiry}"
20
+
21
+ strike_prices = chain_df.map { |r| r["strike"] }.sort
22
+ sell_ce_strike = strike_prices.min_by { |x| (x - (spot + 200)).abs }
23
+ buy_ce_strike = sell_ce_strike + 200
24
+ sell_pe_strike = strike_prices.min_by { |x| (x - (spot - 200)).abs }
25
+ buy_pe_strike = sell_pe_strike - 200
26
+
27
+ def get_row(chain_df, target_strike)
28
+ chain_df.find { |r| r["strike"] == target_strike }
29
+ end
30
+
31
+ sell_ce = get_row(chain_df, sell_ce_strike)
32
+ buy_ce = get_row(chain_df, buy_ce_strike)
33
+ sell_pe = get_row(chain_df, sell_pe_strike)
34
+ buy_pe = get_row(chain_df, buy_pe_strike)
35
+
36
+ if [sell_ce, buy_ce, sell_pe, buy_pe].any?(&:nil?)
37
+ puts "Could not find all required strikes. Try different offsets."
38
+ exit 1
39
+ end
40
+
41
+ lot_size = get_lot_size(underlying: "NIFTY") || 75
42
+ legs = [
43
+ {
44
+ "label" => "Sell #{sell_pe_strike.to_i} PE",
45
+ "type" => "PE",
46
+ "strike" => sell_pe_strike,
47
+ "premium" => sell_pe["pe_ltp"].to_f,
48
+ "qty" => -1,
49
+ "sid" => sell_pe["pe_security_id"]
50
+ },
51
+ {
52
+ "label" => "Buy #{buy_pe_strike.to_i} PE",
53
+ "type" => "PE",
54
+ "strike" => buy_pe_strike,
55
+ "premium" => buy_pe["pe_ltp"].to_f,
56
+ "qty" => 1,
57
+ "sid" => buy_pe["pe_security_id"]
58
+ },
59
+ {
60
+ "label" => "Sell #{sell_ce_strike.to_i} CE",
61
+ "type" => "CE",
62
+ "strike" => sell_ce_strike,
63
+ "premium" => sell_ce["ce_ltp"].to_f,
64
+ "qty" => -1,
65
+ "sid" => sell_ce["ce_security_id"]
66
+ },
67
+ {
68
+ "label" => "Buy #{buy_ce_strike.to_i} CE",
69
+ "type" => "CE",
70
+ "strike" => buy_ce_strike,
71
+ "premium" => buy_ce["ce_ltp"].to_f,
72
+ "qty" => 1,
73
+ "sid" => buy_ce["ce_security_id"]
74
+ }
75
+ ]
76
+
77
+ net_premium = legs.sum { |leg| -leg["qty"] * leg["premium"] }
78
+
79
+ # Evaluate payoffs
80
+ spot_range = []
81
+ current_spot = spot - 1000
82
+ while current_spot <= spot + 1000
83
+ spot_range << current_spot
84
+ current_spot += 10
85
+ end
86
+
87
+ payoffs = spot_range.map do |s|
88
+ payoff_sum = 0.0
89
+ legs.each do |leg|
90
+ intrinsic = if leg["type"] == "CE"
91
+ [s - leg["strike"], 0.0].max
92
+ else
93
+ [leg["strike"] - s, 0.0].max
94
+ end
95
+ payoff_sum += (intrinsic - leg["premium"]) * leg["qty"] * lot_size
96
+ end
97
+ payoff_sum
98
+ end
99
+
100
+ max_profit = payoffs.max
101
+ max_loss = payoffs.min
102
+
103
+ # Find breakevens where sign changes
104
+ breakevens = []
105
+ (0...(payoffs.size - 1)).each do |i|
106
+ next unless (payoffs[i] >= 0 && payoffs[i + 1].negative?) || (payoffs[i].negative? && payoffs[i + 1] >= 0)
107
+
108
+ # Linear interpolation for zero crossing
109
+ x1 = spot_range[i]
110
+ y1 = payoffs[i]
111
+ x2 = spot_range[i + 1]
112
+ y2 = payoffs[i + 1]
113
+ zero_spot = x1 - (y1 * (x2 - x1) / (y2 - y1))
114
+ breakevens << zero_spot
115
+ end
116
+
117
+ puts "\n======================================================="
118
+ puts " NIFTY IRON CONDOR — Expiry: #{nearest_expiry}"
119
+ puts "======================================================="
120
+ puts "\n Legs:"
121
+ legs.each do |leg|
122
+ action = leg["qty"].negative? ? DhanHQ::Constants::TransactionType::SELL : "BUY "
123
+ puts " #{action} 1 lot #{leg["label"]} @ Rs. #{"%.1f" % leg["premium"]}"
124
+ end
125
+
126
+ puts "\n Analysis (1 lot = #{lot_size} qty):"
127
+ printf(" Net Premium: Rs. %8.0f (%s)\n", net_premium * lot_size, net_premium.positive? ? "credit" : "debit")
128
+ printf(" Max Profit: Rs. %8.0f\n", max_profit)
129
+ printf(" Max Loss: Rs. %8.0f\n", max_loss)
130
+ puts " Breakevens: #{breakevens.map { |b| "%.0f" % b }.join(", ")}"
131
+ puts " Risk/Reward: 1:#{format("%.1f", max_profit / max_loss.abs)}" if max_loss != 0
132
+
133
+ puts "\n Orders to place after confirmation:"
134
+ legs.each do |leg|
135
+ action = leg["qty"].negative? ? DhanHQ::Constants::TransactionType::SELL : DhanHQ::Constants::TransactionType::BUY
136
+ puts " #{action} #{lot_size} qty | SID: #{leg["sid"]} | Rs. #{"%.1f" % leg["premium"]}"
137
+ end
@@ -0,0 +1,43 @@
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
+ puts "Starting live market feed... (Ctrl+C to stop)"
11
+
12
+ # Connect in Ticker mode (LTP updates)
13
+ # Other available modes: :quote (OHLC + Volume), :full (full depth)
14
+ market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
15
+ timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
16
+ puts "Tick Received -> Segment: #{tick[:segment]}, SecID: #{tick[:security_id]}, LTP: #{tick[:ltp]} at #{timestamp}"
17
+ end
18
+
19
+ # Subscribe to target instruments
20
+ # segment must match exchange segment constants from Constants, e.g. "NSE_EQ", "IDX_I", etc.
21
+ market_client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ, security_id: "2885") # RELIANCE
22
+ market_client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ, security_id: "1333") # HDFCBANK
23
+ market_client.subscribe_one(segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ, security_id: "11536") # TCS
24
+
25
+ begin
26
+ # Wait for feed to stream ticks
27
+ sleep(15)
28
+ rescue Interrupt
29
+ puts "\nStopping due to interrupt..."
30
+ ensure
31
+ puts "Shutting down WebSocket..."
32
+ begin
33
+ market_client.stop
34
+ rescue StandardError
35
+ nil
36
+ end
37
+ begin
38
+ DhanHQ::WS.disconnect_all_local!
39
+ rescue StandardError
40
+ nil
41
+ end
42
+ puts "Feed stopped."
43
+ end
@@ -0,0 +1,42 @@
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
+ funds = DhanHQ::Models::Funds.fetch
11
+ available = funds.availabel_balance || funds.available_balance || 0.0
12
+ puts "Available Balance: Rs. #{"%.2f" % available}"
13
+
14
+ expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(underlying_scrip: 13, underlying_seg: DhanHQ::Constants::ExchangeSegment::IDX_I)
15
+ nearest_expiry = expiries.first
16
+
17
+ chain_df, spot = fetch_chain_df(under_security_id: 13, expiry: nearest_expiry)
18
+ atm = find_atm_row(chain_df, spot)
19
+
20
+ if atm
21
+ puts "\n--- Margin Check: Buy 1 Lot Nifty CE (INTRADAY) ---"
22
+ option_margin = check_margin(
23
+ security_id: atm["ce_security_id"],
24
+ exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_FNO,
25
+ transaction_type: DhanHQ::Constants::TransactionType::BUY,
26
+ quantity: 75,
27
+ product_type: DhanHQ::Constants::ProductType::INTRADAY,
28
+ price: atm["ce_ltp"].to_f
29
+ )
30
+ puts option_margin.inspect
31
+ end
32
+
33
+ puts "\n--- Margin Check: Buy 10 RELIANCE (CNC Delivery) ---"
34
+ equity_margin = check_margin(
35
+ security_id: "2885",
36
+ exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
37
+ transaction_type: DhanHQ::Constants::TransactionType::BUY,
38
+ quantity: 10,
39
+ product_type: DhanHQ::Constants::ProductType::CNC,
40
+ price: 2450.0
41
+ )
42
+ puts equity_margin.inspect