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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +7 -0
  4. data/CHANGELOG.md +51 -0
  5. data/README.md +152 -4
  6. data/docs/CONSTANTS_REFERENCE.md +3 -2
  7. data/exe/dhanhq-mcp +7 -0
  8. data/lib/DhanHQ/agent/order_preview.rb +50 -0
  9. data/lib/DhanHQ/agent/policy.rb +51 -0
  10. data/lib/DhanHQ/agent/tool_registry.rb +299 -0
  11. data/lib/DhanHQ/agent.rb +12 -0
  12. data/lib/DhanHQ/ai/context_builder.rb +145 -0
  13. data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
  14. data/lib/DhanHQ/ai.rb +27 -0
  15. data/lib/DhanHQ/auth.rb +0 -1
  16. data/lib/DhanHQ/client.rb +1 -3
  17. data/lib/DhanHQ/concerns/order_audit.rb +43 -1
  18. data/lib/DhanHQ/constants.rb +5 -2
  19. data/lib/DhanHQ/contracts/forever_order_contract.rb +1 -1
  20. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
  21. data/lib/DhanHQ/contracts/place_order_contract.rb +1 -1
  22. data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
  23. data/lib/DhanHQ/core/auth_api.rb +0 -1
  24. data/lib/DhanHQ/errors.rb +4 -0
  25. data/lib/DhanHQ/events/base.rb +203 -0
  26. data/lib/DhanHQ/events/bus.rb +158 -0
  27. data/lib/DhanHQ/events.rb +40 -0
  28. data/lib/DhanHQ/indicators.rb +283 -0
  29. data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
  30. data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
  31. data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
  32. data/lib/DhanHQ/market_data.rb +25 -0
  33. data/lib/DhanHQ/mcp/server.rb +235 -0
  34. data/lib/DhanHQ/mcp.rb +10 -0
  35. data/lib/DhanHQ/models/funds.rb +12 -0
  36. data/lib/DhanHQ/models/holding.rb +42 -0
  37. data/lib/DhanHQ/models/iceberg_order.rb +139 -0
  38. data/lib/DhanHQ/models/instrument.rb +80 -14
  39. data/lib/DhanHQ/models/order.rb +95 -0
  40. data/lib/DhanHQ/models/position.rb +66 -0
  41. data/lib/DhanHQ/models/search_result.rb +12 -0
  42. data/lib/DhanHQ/models/trade.rb +13 -0
  43. data/lib/DhanHQ/models/twap_order.rb +136 -0
  44. data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
  45. data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
  46. data/lib/DhanHQ/option_analytics.rb +36 -0
  47. data/lib/DhanHQ/rate_limiter.rb +5 -3
  48. data/lib/DhanHQ/resources/alert_orders.rb +1 -0
  49. data/lib/DhanHQ/resources/forever_orders.rb +1 -0
  50. data/lib/DhanHQ/resources/iceberg_orders.rb +62 -0
  51. data/lib/DhanHQ/resources/orders.rb +2 -0
  52. data/lib/DhanHQ/resources/pnl_exit.rb +1 -0
  53. data/lib/DhanHQ/resources/super_orders.rb +1 -0
  54. data/lib/DhanHQ/resources/twap_orders.rb +62 -0
  55. data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
  56. data/lib/DhanHQ/risk/checks/concentration.rb +37 -0
  57. data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
  58. data/lib/DhanHQ/risk/checks/max_loss.rb +24 -0
  59. data/lib/DhanHQ/risk/checks/options.rb +46 -0
  60. data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
  61. data/lib/DhanHQ/risk/checks/position_limits.rb +24 -0
  62. data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
  63. data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
  64. data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
  65. data/lib/DhanHQ/risk/pipeline.rb +72 -0
  66. data/lib/DhanHQ/risk.rb +250 -0
  67. data/lib/DhanHQ/skills/base.rb +183 -0
  68. data/lib/DhanHQ/skills/builtin/bear_call_spread.rb +87 -0
  69. data/lib/DhanHQ/skills/builtin/bull_put_spread.rb +87 -0
  70. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +84 -0
  71. data/lib/DhanHQ/skills/builtin/covered_call.rb +85 -0
  72. data/lib/DhanHQ/skills/builtin/iron_condor.rb +89 -0
  73. data/lib/DhanHQ/skills/builtin/market_data_summarizer.rb +195 -0
  74. data/lib/DhanHQ/skills/builtin/protective_put.rb +90 -0
  75. data/lib/DhanHQ/skills/builtin/square_off_all.rb +42 -0
  76. data/lib/DhanHQ/skills/builtin/square_off_position.rb +50 -0
  77. data/lib/DhanHQ/skills/builtin/straddle.rb +88 -0
  78. data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
  79. data/lib/DhanHQ/skills/registry.rb +101 -0
  80. data/lib/DhanHQ/skills/workflow.rb +66 -0
  81. data/lib/DhanHQ/skills.rb +29 -0
  82. data/lib/DhanHQ/strategy/base.rb +189 -0
  83. data/lib/DhanHQ/strategy.rb +40 -0
  84. data/lib/DhanHQ/version.rb +1 -1
  85. data/lib/DhanHQ/ws/decoder.rb +57 -19
  86. data/lib/DhanHQ.rb +3 -0
  87. data/lib/dhan_hq/agent.rb +3 -0
  88. data/lib/dhan_hq/mcp.rb +3 -0
  89. data/lib/dhan_hq.rb +74 -2
  90. data/lib/ta/technical_analysis.rb +3 -1
  91. data/skills/dhanhq-ruby/SKILL.md +207 -0
  92. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +54 -0
  93. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +65 -0
  94. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +89 -0
  95. data/skills/dhanhq-ruby/examples/iron_condor.rb +137 -0
  96. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +43 -0
  97. data/skills/dhanhq-ruby/examples/margin_check.rb +42 -0
  98. data/skills/dhanhq-ruby/examples/order_management.rb +105 -0
  99. data/skills/dhanhq-ruby/examples/place_equity_order.rb +36 -0
  100. data/skills/dhanhq-ruby/examples/place_fno_order.rb +76 -0
  101. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +74 -0
  102. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +57 -0
  103. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +65 -0
  104. data/skills/dhanhq-ruby/references/common-workflows.md +76 -0
  105. data/skills/dhanhq-ruby/references/error-codes.md +50 -0
  106. data/skills/dhanhq-ruby/references/funds.md +67 -0
  107. data/skills/dhanhq-ruby/references/instruments.md +85 -0
  108. data/skills/dhanhq-ruby/references/live-feed.md +83 -0
  109. data/skills/dhanhq-ruby/references/market-data.md +119 -0
  110. data/skills/dhanhq-ruby/references/option-chain.md +71 -0
  111. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +76 -0
  112. data/skills/dhanhq-ruby/references/orders.md +201 -0
  113. data/skills/dhanhq-ruby/references/portfolio.md +93 -0
  114. data/skills/dhanhq-ruby/references/scanx-data.md +62 -0
  115. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +323 -0
  116. data/skills/dhanhq-ruby/scripts/resolve_security.rb +168 -0
  117. data/skills/dhanhq-ruby/scripts/trade_logger.rb +131 -0
  118. data/skills/dhanhq-ruby/scripts/validate_order.rb +169 -0
  119. 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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.0.0"
5
+ VERSION = "3.1.0"
6
6
  end
@@ -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
- # pp pkt
30
- case kind
31
- when :ticker
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
- when :quote
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
- when :full
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
- # First depth level (if present)
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
- when :oi
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
- when :prev_close
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
- when :depth_bid, :depth_ask
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
- when :disconnect
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
- else
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
- rescue StandardError => e
78
- DhanHQ.logger&.debug("[DhanHQ::WS::Decoder] #{e.class}: #{e.message}")
79
- nil
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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dhan_hq"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../DhanHQ/agent"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../DhanHQ/mcp"
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.response :json, content_type: /\bjson$/
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
- n_back = [days_back.to_i - 1, 0].max
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) |