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.
- checksums.yaml +4 -4
- data/AGENTS.md +23 -0
- data/CHANGELOG.md +74 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +16 -0
- data/README.md +54 -1
- data/docs/CONFIGURATION.md +38 -14
- data/docs/RELEASE_GUIDE.md +1 -1
- data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +53 -34
- data/lib/DhanHQ/concerns/bang_writes.rb +69 -0
- data/lib/DhanHQ/concerns/tracked_writes.rb +56 -0
- data/lib/DhanHQ/configuration.rb +20 -1
- data/lib/DhanHQ/core/base_model.rb +6 -13
- data/lib/DhanHQ/deprecation.rb +62 -0
- data/lib/DhanHQ/models/alert_order.rb +8 -1
- data/lib/DhanHQ/models/forever_order.rb +9 -0
- data/lib/DhanHQ/models/global_stocks/order.rb +9 -0
- data/lib/DhanHQ/models/iceberg_order.rb +9 -0
- data/lib/DhanHQ/models/multi_order.rb +7 -0
- data/lib/DhanHQ/models/order.rb +28 -0
- data/lib/DhanHQ/models/pnl_exit.rb +7 -0
- data/lib/DhanHQ/models/super_order.rb +9 -0
- data/lib/DhanHQ/models/twap_order.rb +9 -0
- data/lib/DhanHQ/risk/pipeline.rb +0 -2
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/write_result.rb +163 -0
- data/lib/dhan_hq.rb +9 -0
- data/skills/dhanhq-ruby/SKILL.md +208 -0
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
- data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
- data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
- data/skills/dhanhq-ruby/examples/order_management.rb +112 -0
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
- data/skills/dhanhq-ruby/references/error-codes.md +50 -0
- data/skills/dhanhq-ruby/references/funds.md +67 -0
- data/skills/dhanhq-ruby/references/instruments.md +91 -0
- data/skills/dhanhq-ruby/references/live-feed.md +83 -0
- data/skills/dhanhq-ruby/references/market-data.md +119 -0
- data/skills/dhanhq-ruby/references/option-chain.md +71 -0
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
- data/skills/dhanhq-ruby/references/orders.md +203 -0
- data/skills/dhanhq-ruby/references/portfolio.md +100 -0
- data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
- data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
- 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
|