DhanHQ 3.0.0 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +51 -0
- data/README.md +152 -4
- data/docs/CONSTANTS_REFERENCE.md +3 -2
- data/exe/dhanhq-mcp +7 -0
- data/lib/DhanHQ/agent/order_preview.rb +50 -0
- data/lib/DhanHQ/agent/policy.rb +51 -0
- data/lib/DhanHQ/agent/tool_registry.rb +299 -0
- data/lib/DhanHQ/agent.rb +12 -0
- data/lib/DhanHQ/ai/context_builder.rb +145 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
- data/lib/DhanHQ/ai.rb +27 -0
- data/lib/DhanHQ/auth.rb +0 -1
- data/lib/DhanHQ/client.rb +1 -3
- data/lib/DhanHQ/concerns/order_audit.rb +43 -1
- data/lib/DhanHQ/constants.rb +5 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
- data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
- data/lib/DhanHQ/core/auth_api.rb +0 -1
- data/lib/DhanHQ/errors.rb +4 -0
- data/lib/DhanHQ/events/base.rb +203 -0
- data/lib/DhanHQ/events/bus.rb +158 -0
- data/lib/DhanHQ/events.rb +40 -0
- data/lib/DhanHQ/indicators.rb +283 -0
- data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
- data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
- data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
- data/lib/DhanHQ/market_data.rb +25 -0
- data/lib/DhanHQ/mcp/server.rb +235 -0
- data/lib/DhanHQ/mcp.rb +10 -0
- data/lib/DhanHQ/models/funds.rb +12 -0
- data/lib/DhanHQ/models/holding.rb +42 -0
- data/lib/DhanHQ/models/iceberg_order.rb +139 -0
- data/lib/DhanHQ/models/instrument.rb +80 -14
- data/lib/DhanHQ/models/order.rb +95 -0
- data/lib/DhanHQ/models/position.rb +66 -0
- data/lib/DhanHQ/models/search_result.rb +12 -0
- data/lib/DhanHQ/models/trade.rb +13 -0
- data/lib/DhanHQ/models/twap_order.rb +136 -0
- data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
- data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
- data/lib/DhanHQ/option_analytics.rb +36 -0
- data/lib/DhanHQ/rate_limiter.rb +5 -3
- data/lib/DhanHQ/resources/alert_orders.rb +1 -0
- data/lib/DhanHQ/resources/forever_orders.rb +1 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +62 -0
- data/lib/DhanHQ/resources/orders.rb +2 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
- data/lib/DhanHQ/resources/super_orders.rb +1 -0
- data/lib/DhanHQ/resources/twap_orders.rb +62 -0
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
- data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
- data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
- data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
- data/lib/DhanHQ/risk/checks/options.rb +46 -0
- data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
- data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
- data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
- data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
- data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
- data/lib/DhanHQ/risk/pipeline.rb +72 -0
- data/lib/DhanHQ/risk.rb +250 -0
- data/lib/DhanHQ/skills/base.rb +183 -0
- data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +84 -0
- data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +89 -0
- data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
- data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +42 -0
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +50 -0
- data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
- data/lib/DhanHQ/skills/registry.rb +101 -0
- data/lib/DhanHQ/skills/workflow.rb +66 -0
- data/lib/DhanHQ/skills.rb +29 -0
- data/lib/DhanHQ/strategy/base.rb +189 -0
- data/lib/DhanHQ/strategy.rb +40 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/decoder.rb +57 -19
- data/lib/DhanHQ.rb +3 -0
- data/lib/dhan_hq/agent.rb +3 -0
- data/lib/dhan_hq/mcp.rb +3 -0
- data/lib/dhan_hq.rb +74 -2
- data/lib/ta/technical_analysis.rb +3 -1
- data/skills/dhanhq-ruby/SKILL.md +207 -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 +105 -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 +85 -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 +201 -0
- data/skills/dhanhq-ruby/references/portfolio.md +93 -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 +95 -20
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Strategy framework for building and backtesting trading strategies.
|
|
5
|
+
#
|
|
6
|
+
# Provides a DSL for defining entry/exit rules, risk management,
|
|
7
|
+
# and signal generation.
|
|
8
|
+
#
|
|
9
|
+
# @example Define a simple strategy
|
|
10
|
+
# class MyStrategy < DhanHQ::Strategy::Base
|
|
11
|
+
# entry_rule :golden_cross do |data|
|
|
12
|
+
# data.sma(20).last > data.sma(50).last &&
|
|
13
|
+
# data.sma(20).last(2) <= data.sma(50).last(2)
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# exit_rule :death_cross do |data|
|
|
17
|
+
# data.sma(20).last < data.sma(50).last
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module Strategy
|
|
22
|
+
# Signal represents a trading signal generated by a strategy.
|
|
23
|
+
#
|
|
24
|
+
# @attr_reader [Symbol] type Signal type (:buy, :sell, :hold)
|
|
25
|
+
# @attr_reader [Float] strength Signal strength (0.0 to 1.0)
|
|
26
|
+
# @attr_reader [Hash] metadata Additional signal metadata
|
|
27
|
+
Signal = Struct.new(:type, :strength, :metadata) do
|
|
28
|
+
def buy?
|
|
29
|
+
type == :buy
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sell?
|
|
33
|
+
type == :sell
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def hold?
|
|
37
|
+
type == :hold
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_prompt
|
|
41
|
+
parts = ["signal=#{type}", "strength=#{strength}"]
|
|
42
|
+
parts << "reason=#{metadata[:reason]}" if metadata[:reason]
|
|
43
|
+
parts.join(", ")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Base class for all strategies.
|
|
48
|
+
#
|
|
49
|
+
# Provides DSL for defining entry/exit rules and risk management.
|
|
50
|
+
class Base
|
|
51
|
+
attr_reader :name, :params, :position
|
|
52
|
+
|
|
53
|
+
def initialize(name: self.class.name, params: {})
|
|
54
|
+
@name = name
|
|
55
|
+
@params = params
|
|
56
|
+
@position = nil
|
|
57
|
+
@entry_rules = {}
|
|
58
|
+
@exit_rules = {}
|
|
59
|
+
@risk_rules = {}
|
|
60
|
+
@signals = []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Define an entry rule.
|
|
64
|
+
#
|
|
65
|
+
# @param name [Symbol] Rule name
|
|
66
|
+
# @param priority [Integer] Rule priority (lower = higher priority)
|
|
67
|
+
# @param block [Proc] Rule condition block
|
|
68
|
+
def self.entry_rule(name, priority: 10, &block)
|
|
69
|
+
@entry_rules ||= {}
|
|
70
|
+
@entry_rules[name] = { priority: priority, block: block }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Define an exit rule.
|
|
74
|
+
#
|
|
75
|
+
# @param name [Symbol] Rule name
|
|
76
|
+
# @param priority [Integer] Rule priority (lower = higher priority)
|
|
77
|
+
# @param block [Proc] Rule condition block
|
|
78
|
+
def self.exit_rule(name, priority: 10, &block)
|
|
79
|
+
@exit_rules ||= {}
|
|
80
|
+
@exit_rules[name] = { priority: priority, block: block }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Define a risk rule.
|
|
84
|
+
#
|
|
85
|
+
# @param name [Symbol] Rule name
|
|
86
|
+
# @param block [Proc] Risk check block
|
|
87
|
+
def self.risk_rule(name, &block)
|
|
88
|
+
@risk_rules ||= {}
|
|
89
|
+
@risk_rules[name] = block
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get all entry rules for this strategy class.
|
|
93
|
+
def self.entry_rules
|
|
94
|
+
@entry_rules || {}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get all exit rules for this strategy class.
|
|
98
|
+
def self.exit_rules
|
|
99
|
+
@exit_rules || {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get all risk rules for this strategy class.
|
|
103
|
+
def self.risk_rules
|
|
104
|
+
@risk_rules || {}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Evaluate entry rules against market data.
|
|
108
|
+
#
|
|
109
|
+
# @param data [DhanHQ::MarketData::OHLCSeries] Market data series
|
|
110
|
+
# @return [DhanHQ::Strategy::Signal] Generated signal
|
|
111
|
+
def evaluate_entry(data)
|
|
112
|
+
sorted_rules = self.class.entry_rules.sort_by { |_, v| v[:priority] }
|
|
113
|
+
|
|
114
|
+
sorted_rules.each do |rule_name, rule|
|
|
115
|
+
result = rule[:block].call(data, params)
|
|
116
|
+
next unless result
|
|
117
|
+
|
|
118
|
+
signal = Signal.new(
|
|
119
|
+
type: :buy,
|
|
120
|
+
strength: calculate_strength(result),
|
|
121
|
+
metadata: { rule: rule_name, data: data }
|
|
122
|
+
)
|
|
123
|
+
@signals << signal
|
|
124
|
+
return signal
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
Signal.new(type: :hold, strength: 0.0, metadata: { reason: :no_entry_signal })
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Evaluate exit rules against market data.
|
|
131
|
+
#
|
|
132
|
+
# @param data [DhanHQ::MarketData::OHLCSeries] Market data series
|
|
133
|
+
# @return [DhanHQ::Strategy::Signal] Generated signal
|
|
134
|
+
def evaluate_exit(data)
|
|
135
|
+
sorted_rules = self.class.exit_rules.sort_by { |_, v| v[:priority] }
|
|
136
|
+
|
|
137
|
+
sorted_rules.each do |rule_name, rule|
|
|
138
|
+
result = rule[:block].call(data, params)
|
|
139
|
+
next unless result
|
|
140
|
+
|
|
141
|
+
signal = Signal.new(
|
|
142
|
+
type: :sell,
|
|
143
|
+
strength: calculate_strength(result),
|
|
144
|
+
metadata: { rule: rule_name, data: data }
|
|
145
|
+
)
|
|
146
|
+
@signals << signal
|
|
147
|
+
return signal
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Signal.new(type: :hold, strength: 0.0, metadata: { reason: :no_exit_signal })
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check all risk rules.
|
|
154
|
+
#
|
|
155
|
+
# @param context [Hash] Risk check context
|
|
156
|
+
# @return [Array<Symbol>] List of violated rule names
|
|
157
|
+
def check_risks(context)
|
|
158
|
+
violations = []
|
|
159
|
+
|
|
160
|
+
self.class.risk_rules.each do |rule_name, block|
|
|
161
|
+
result = block.call(context, params)
|
|
162
|
+
violations << rule_name unless result
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
violations
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get all signals generated by this strategy.
|
|
169
|
+
def signals
|
|
170
|
+
@signals.dup
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get the last signal.
|
|
174
|
+
def last_signal
|
|
175
|
+
@signals.last
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def calculate_strength(result)
|
|
181
|
+
case result
|
|
182
|
+
when true then 1.0
|
|
183
|
+
when Numeric then result.clamp(0.0, 1.0)
|
|
184
|
+
else 0.5
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "strategy/base"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
# Strategy framework for building and backtesting trading strategies.
|
|
7
|
+
#
|
|
8
|
+
# Provides a DSL for defining entry/exit rules, risk management,
|
|
9
|
+
# and signal generation.
|
|
10
|
+
#
|
|
11
|
+
# @example Define a simple strategy
|
|
12
|
+
# class GoldenCross < DhanHQ::Strategy::Base
|
|
13
|
+
# entry_rule :golden_cross do |data, _params|
|
|
14
|
+
# sma_20 = DhanHQ::Indicators::SMA.calculate(data.closes, period: 20)
|
|
15
|
+
# sma_50 = DhanHQ::Indicators::SMA.calculate(data.closes, period: 50)
|
|
16
|
+
#
|
|
17
|
+
# sma_20.last && sma_50.last &&
|
|
18
|
+
# sma_20.last > sma_50.last &&
|
|
19
|
+
# sma_20[-2] && sma_50[-2] &&
|
|
20
|
+
# sma_20[-2] <= sma_50[-2]
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# exit_rule :death_cross do |data, _params|
|
|
24
|
+
# sma_20 = DhanHQ::Indicators::SMA.calculate(data.closes, period: 20)
|
|
25
|
+
# sma_50 = DhanHQ::Indicators::SMA.calculate(data.closes, period: 50)
|
|
26
|
+
#
|
|
27
|
+
# sma_20.last && sma_50.last && sma_20.last < sma_50.last
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# risk_rule :max_drawdown do |context, _params|
|
|
31
|
+
# context[:drawdown].abs < 0.1
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# strategy = GoldenCross.new
|
|
36
|
+
# signal = strategy.evaluate_entry(data)
|
|
37
|
+
#
|
|
38
|
+
module Strategy
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/decoder.rb
CHANGED
|
@@ -26,21 +26,45 @@ module DhanHQ
|
|
|
26
26
|
segstr = Segments.from_code(pkt[:exchange_segment])
|
|
27
27
|
sid = pkt[:security_id].to_s
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
dispatch(pkt, kind, segstr, sid)
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
DhanHQ.logger&.debug("[DhanHQ::WS::Decoder] #{e.class}: #{e.message}")
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def dispatch(pkt, kind, segstr, sid)
|
|
39
|
+
case kind
|
|
40
|
+
when :ticker then decode_ticker(pkt, segstr, sid)
|
|
41
|
+
when :quote then decode_quote(pkt, segstr, sid)
|
|
42
|
+
when :full then decode_full(pkt, segstr, sid)
|
|
43
|
+
when :oi then decode_oi(pkt, segstr, sid)
|
|
44
|
+
when :prev_close then decode_prev_close(pkt, segstr, sid)
|
|
45
|
+
when :depth_bid, :depth_ask then decode_depth(pkt, kind, segstr, sid)
|
|
46
|
+
when :disconnect then handle_disconnect(pkt, segstr, sid)
|
|
47
|
+
else handle_unknown(pkt)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def decode_ticker(pkt, segstr, sid)
|
|
32
52
|
{
|
|
33
53
|
kind: :ticker, segment: segstr, security_id: sid,
|
|
34
54
|
ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i
|
|
35
55
|
}
|
|
36
|
-
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def decode_quote(pkt, segstr, sid)
|
|
37
59
|
{
|
|
38
60
|
kind: :quote, segment: segstr, security_id: sid,
|
|
39
61
|
ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i, atp: pkt[:atp].to_f,
|
|
40
62
|
vol: pkt[:volume].to_i, ts_buy_qty: pkt[:total_buy_qty].to_i, ts_sell_qty: pkt[:total_sell_qty].to_i,
|
|
41
63
|
day_open: pkt[:day_open]&.to_f, day_high: pkt[:day_high]&.to_f, day_low: pkt[:day_low]&.to_f, day_close: pkt[:day_close]&.to_f
|
|
42
64
|
}
|
|
43
|
-
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def decode_full(pkt, segstr, sid)
|
|
44
68
|
out = {
|
|
45
69
|
kind: :full, segment: segstr, security_id: sid,
|
|
46
70
|
ltp: pkt[:ltp].to_f, ts: pkt[:ltt]&.to_i, atp: pkt[:atp].to_f,
|
|
@@ -48,35 +72,49 @@ module DhanHQ
|
|
|
48
72
|
oi: pkt[:open_interest]&.to_i, oi_high: pkt[:highest_open_interest]&.to_i, oi_low: pkt[:lowest_open_interest]&.to_i,
|
|
49
73
|
day_open: pkt[:day_open]&.to_f, day_high: pkt[:day_high]&.to_f, day_low: pkt[:day_low]&.to_f, day_close: pkt[:day_close]&.to_f
|
|
50
74
|
}
|
|
51
|
-
|
|
52
|
-
if (md = pkt[:market_depth]).respond_to?(:[]) && md[0]
|
|
53
|
-
lvl = md[0]
|
|
54
|
-
out[:bid] = lvl.respond_to?(:bid_price) ? lvl.bid_price.to_f : nil
|
|
55
|
-
out[:ask] = lvl.respond_to?(:ask_price) ? lvl.ask_price.to_f : nil
|
|
56
|
-
end
|
|
75
|
+
merge_depth(out, pkt[:market_depth])
|
|
57
76
|
out
|
|
58
|
-
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def decode_oi(pkt, segstr, sid)
|
|
59
80
|
{ kind: :oi, segment: segstr, security_id: sid, oi: pkt[:open_interest].to_i }
|
|
60
|
-
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def decode_prev_close(pkt, segstr, sid)
|
|
61
84
|
{ kind: :prev_close, segment: segstr, security_id: sid, prev_close: pkt[:prev_close].to_f,
|
|
62
85
|
oi_prev: pkt[:oi_prev].to_i }
|
|
63
|
-
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def decode_depth(pkt, kind, segstr, sid)
|
|
64
89
|
{
|
|
65
90
|
kind: kind, segment: segstr, security_id: sid,
|
|
66
91
|
bid_quantity: pkt[:bid_quantity], ask_quantity: pkt[:ask_quantity],
|
|
67
92
|
no_of_bid_orders: pkt[:no_of_bid_orders], no_of_ask_orders: pkt[:no_of_ask_orders],
|
|
68
93
|
bid: pkt[:bid_price], ask: pkt[:ask_price]
|
|
69
94
|
}
|
|
70
|
-
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_disconnect(pkt, segstr, sid)
|
|
71
98
|
DhanHQ.logger&.warn("[DhanHQ::WS] disconnect code=#{pkt[:disconnection_code]} seg=#{segstr} sid=#{sid}")
|
|
72
99
|
nil
|
|
73
|
-
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def handle_unknown(pkt)
|
|
74
103
|
DhanHQ.logger&.debug("[DhanHQ::WS] unknown feed kind code=#{pkt[:feed_response_code]}")
|
|
75
104
|
nil
|
|
76
105
|
end
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
|
|
107
|
+
def merge_depth(out, market_depth)
|
|
108
|
+
return unless market_depth.respond_to?(:[]) && market_depth[0]
|
|
109
|
+
|
|
110
|
+
lvl = market_depth[0]
|
|
111
|
+
out[:bid] = lvl.respond_to?(:bid_price) ? lvl.bid_price.to_f : nil
|
|
112
|
+
out[:ask] = lvl.respond_to?(:ask_price) ? lvl.ask_price.to_f : nil
|
|
113
|
+
return unless lvl.respond_to?(:bid_quantity) && lvl.respond_to?(:ask_quantity)
|
|
114
|
+
|
|
115
|
+
out[:bid_qty] = lvl.bid_quantity.to_i
|
|
116
|
+
out[:ask_qty] = lvl.ask_quantity.to_i
|
|
117
|
+
end
|
|
80
118
|
end
|
|
81
119
|
end
|
|
82
120
|
end
|
data/lib/DhanHQ.rb
ADDED
data/lib/dhan_hq/mcp.rb
ADDED
data/lib/dhan_hq.rb
CHANGED
|
@@ -38,10 +38,39 @@ module DhanHQ
|
|
|
38
38
|
LOADER.collapse(File.join(__dir__, "DhanHQ", "helpers"))
|
|
39
39
|
LOADER.ignore(
|
|
40
40
|
File.join(__dir__, "DhanHQ", "errors.rb"),
|
|
41
|
-
File.join(__dir__, "DhanHQ", "version.rb")
|
|
41
|
+
File.join(__dir__, "DhanHQ", "version.rb"),
|
|
42
|
+
File.join(__dir__, "DhanHQ", "risk.rb")
|
|
42
43
|
)
|
|
43
44
|
LOADER.setup
|
|
44
45
|
|
|
46
|
+
# Eager-load risk utilities (PositionSizer, SLCalculator, TrailManager)
|
|
47
|
+
# and the pre-execution risk pipeline so they are available immediately.
|
|
48
|
+
require_relative "DhanHQ/risk"
|
|
49
|
+
require_relative "DhanHQ/risk/checks/trading_permission"
|
|
50
|
+
require_relative "DhanHQ/risk/checks/asm_gsm"
|
|
51
|
+
require_relative "DhanHQ/risk/checks/product_support"
|
|
52
|
+
require_relative "DhanHQ/risk/checks/order_type"
|
|
53
|
+
require_relative "DhanHQ/risk/checks/quantity"
|
|
54
|
+
require_relative "DhanHQ/risk/checks/market_hours"
|
|
55
|
+
require_relative "DhanHQ/risk/checks/options"
|
|
56
|
+
require_relative "DhanHQ/risk/checks/position_limits"
|
|
57
|
+
require_relative "DhanHQ/risk/checks/concentration"
|
|
58
|
+
require_relative "DhanHQ/risk/checks/max_loss"
|
|
59
|
+
require_relative "DhanHQ/risk/pipeline"
|
|
60
|
+
|
|
61
|
+
# Skills layer: multi-step trading workflows
|
|
62
|
+
require_relative "DhanHQ/skills"
|
|
63
|
+
require_relative "DhanHQ/skills/base"
|
|
64
|
+
require_relative "DhanHQ/skills/registry"
|
|
65
|
+
require_relative "DhanHQ/skills/workflow"
|
|
66
|
+
require_relative "DhanHQ/skills/builtin/buy_atm_call"
|
|
67
|
+
require_relative "DhanHQ/skills/builtin/square_off_all"
|
|
68
|
+
require_relative "DhanHQ/skills/builtin/square_off_position"
|
|
69
|
+
require_relative "DhanHQ/skills/builtin/iron_condor"
|
|
70
|
+
require_relative "DhanHQ/skills/builtin/strangle"
|
|
71
|
+
require_relative "DhanHQ/skills/builtin/market_data_summarizer"
|
|
72
|
+
DhanHQ::Skills::Registry.load_builtins
|
|
73
|
+
|
|
45
74
|
class Error < StandardError; end
|
|
46
75
|
|
|
47
76
|
class << self
|
|
@@ -131,7 +160,7 @@ module DhanHQ
|
|
|
131
160
|
|
|
132
161
|
url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
|
|
133
162
|
conn = ::Faraday.new(url: url) do |c|
|
|
134
|
-
c.
|
|
163
|
+
c.request :url_encoded
|
|
135
164
|
c.adapter ::Faraday.default_adapter
|
|
136
165
|
end
|
|
137
166
|
|
|
@@ -161,6 +190,49 @@ module DhanHQ
|
|
|
161
190
|
configuration
|
|
162
191
|
end
|
|
163
192
|
|
|
193
|
+
# Configures the DhanHQ client by fetching credentials from a dashboard API.
|
|
194
|
+
#
|
|
195
|
+
# @param bearer_token [String] Secret dashboard token (e.g. YOUR_DASHBOARD_TOKEN)
|
|
196
|
+
# @param url [String] The full URL of the dashboard API
|
|
197
|
+
# @return [DhanHQ::Configuration] The configured configuration
|
|
198
|
+
# @raise [DhanHQ::TokenEndpointError] On HTTP error or missing credentials
|
|
199
|
+
def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
|
|
200
|
+
raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
|
|
201
|
+
|
|
202
|
+
conn = ::Faraday.new(url: url) do |c|
|
|
203
|
+
c.request :url_encoded
|
|
204
|
+
c.adapter ::Faraday.default_adapter
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
response = conn.get("") do |req|
|
|
208
|
+
req.headers["Authorization"] = "Bearer #{bearer_token}"
|
|
209
|
+
req.headers["Accept"] = "application/json"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
unless response.success?
|
|
213
|
+
body = parse_json_body(response.body)
|
|
214
|
+
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
215
|
+
raise DhanHQ::TokenEndpointError, "Dashboard returned #{response.status}: #{msg}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
data = parse_json_body(response.body)
|
|
219
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
220
|
+
|
|
221
|
+
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
222
|
+
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
223
|
+
client_id ||= self.configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
224
|
+
|
|
225
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
226
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
227
|
+
|
|
228
|
+
self.configuration ||= Configuration.new
|
|
229
|
+
configuration.access_token = access_token.to_s
|
|
230
|
+
configuration.client_id = client_id.to_s
|
|
231
|
+
dhan_base = data["base_url"] || data["baseUrl"]
|
|
232
|
+
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
233
|
+
configuration
|
|
234
|
+
end
|
|
235
|
+
|
|
164
236
|
# @param body [String, Hash] Raw response body
|
|
165
237
|
# @return [Hash] Parsed hash; empty hash on parse failure or empty string
|
|
166
238
|
def parse_json_body(body)
|
|
@@ -125,7 +125,9 @@ module TA
|
|
|
125
125
|
def normalize_from_date(from_date, to_date, days_back)
|
|
126
126
|
if (from_date.nil? || from_date.to_s.strip.empty?) && days_back&.to_i&.positive?
|
|
127
127
|
to_d = Date.parse(to_date)
|
|
128
|
-
|
|
128
|
+
# Dhan historical API to_date is non-inclusive. To get N trading days,
|
|
129
|
+
# we must go back N trading days from the non-inclusive to_date.
|
|
130
|
+
n_back = days_back.to_i
|
|
129
131
|
return MarketCalendar.trading_days_ago(to_d, n_back).strftime("%Y-%m-%d")
|
|
130
132
|
end
|
|
131
133
|
if from_date && !from_date.to_s.strip.empty?
|
|
@@ -0,0 +1,207 @@
|
|
|
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 | `DhanHQ::Models::Instrument.find(exchange_segment, symbol, options)` |
|
|
108
|
+
| Find instrument anywhere | `DhanHQ::Models::Instrument.find_anywhere(symbol, options)` |
|
|
109
|
+
| Super orders | `DhanHQ::Models::SuperOrder.create(params)` |
|
|
110
|
+
| Forever orders | `DhanHQ::Models::ForeverOrder.create(params)` |
|
|
111
|
+
| Live market feed | `DhanHQ::WS.connect(mode: :ticker) { |tick| ... }` |
|
|
112
|
+
| Live order updates | `DhanHQ::WS::Orders.connect { |update| ... }` |
|
|
113
|
+
| Live market depth | `DhanHQ::WS::MarketDepth.connect(symbols: [{...}]) { |depth| ... }` |
|
|
114
|
+
|
|
115
|
+
## High-Value Gotchas
|
|
116
|
+
|
|
117
|
+
- **Return values are model instances:** Most class methods return `DhanHQ::Models` objects rather than raw HTTP hashes.
|
|
118
|
+
- **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.
|
|
119
|
+
- **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`).
|
|
120
|
+
- **Timestamps:** Timestamps returned by `HistoricalData` are automatically normalized into Ruby `Time` objects.
|
|
121
|
+
- **WebSocket connection management:** Use sequential connections or single connection pools to avoid 429 rate limiting. Dhan allows up to 5 concurrent WebSocket connections.
|
|
122
|
+
|
|
123
|
+
## Core Patterns
|
|
124
|
+
|
|
125
|
+
### 1. Check account access before data calls
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
funds = DhanHQ::Models::Funds.fetch
|
|
129
|
+
puts "Available Balance: Rs. #{funds.availabel_balance}"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 2. Fetch historical data
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
candles = DhanHQ::Models::HistoricalData.daily(
|
|
136
|
+
security_id: "2885",
|
|
137
|
+
exchange_segment: "NSE_EQ",
|
|
138
|
+
instrument: "EQUITY",
|
|
139
|
+
from_date: "2024-01-01",
|
|
140
|
+
to_date: "2024-12-31"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
candles.each do |candle|
|
|
144
|
+
puts "Date: #{candle[:timestamp].to_date}, Close: #{candle[:close]}"
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. Option-chain data
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
chain = DhanHQ::Models::OptionChain.fetch(
|
|
152
|
+
underlying_scrip: 13,
|
|
153
|
+
underlying_seg: "IDX_I",
|
|
154
|
+
expiry: "2025-03-27"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
puts "Underlying spot: #{chain[:last_price]}"
|
|
158
|
+
chain[:strikes].each do |strike_data|
|
|
159
|
+
puts "Strike: #{strike_data[:strike]}, Call LTP: #{strike_data[:call][:last_price]}"
|
|
160
|
+
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 4. Margin check before live order placement
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
margin = DhanHQ::Models::Margin.calculate(
|
|
167
|
+
security_id: "2885",
|
|
168
|
+
exchange_segment: "NSE_EQ",
|
|
169
|
+
transaction_type: "BUY",
|
|
170
|
+
quantity: 10,
|
|
171
|
+
product_type: "CNC",
|
|
172
|
+
price: 2450.0
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
puts "Sufficient balance? #{margin.available_balance >= margin.total_margin}"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 5. Live market feed
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
|
182
|
+
puts "Market Tick: #{tick[:security_id]} = #{tick[:ltp]}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
market_client.subscribe_one(segment: "NSE_EQ", security_id: "2885")
|
|
186
|
+
sleep(10)
|
|
187
|
+
market_client.stop
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Reference Files
|
|
191
|
+
|
|
192
|
+
Refer to the documents in the references directory for focused workflows:
|
|
193
|
+
|
|
194
|
+
| Need | File |
|
|
195
|
+
|------|------|
|
|
196
|
+
| Orders, super orders, forever orders | [references/orders.md](references/orders.md) |
|
|
197
|
+
| Holdings, positions, eDIS | [references/portfolio.md](references/portfolio.md) |
|
|
198
|
+
| Daily/minute history, quotes, expired options | [references/market-data.md](references/market-data.md) |
|
|
199
|
+
| Option-chain usage | [references/option-chain.md](references/option-chain.md) |
|
|
200
|
+
| Fund limits and margin checks | [references/funds.md](references/funds.md) |
|
|
201
|
+
| Live feeds and depth | [references/live-feed.md](references/live-feed.md) |
|
|
202
|
+
| Error handling | [references/error-codes.md](references/error-codes.md) |
|
|
203
|
+
| Instrument resolution | [references/instruments.md](references/instruments.md) |
|
|
204
|
+
| Multi-step execution patterns | [references/common-workflows.md](references/common-workflows.md) |
|
|
205
|
+
| Options analytics | [references/options-analysis-patterns.md](references/options-analysis-patterns.md) |
|
|
206
|
+
| Backtesting patterns | [references/backtesting-with-dhan.md](references/backtesting-with-dhan.md) |
|
|
207
|
+
| Extranal data sources (RSI, PE ratios, screener) | [references/scanx-data.md](references/scanx-data.md) |
|