DhanHQ 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +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 +250 -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/constants.rb +2 -0
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
- 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 +72 -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 +36 -0
- 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/resources/iceberg_orders.rb +61 -0
- data/lib/DhanHQ/resources/twap_orders.rb +61 -0
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
- data/lib/DhanHQ/risk/checks/market_hours.rb +37 -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/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 +65 -0
- data/lib/DhanHQ/risk.rb +250 -0
- data/lib/DhanHQ/skills/base.rb +132 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +87 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +93 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +45 -0
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +48 -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 +27 -2
- data/lib/ta/technical_analysis.rb +3 -1
- data/skills/dhanhq-ruby/SKILL.md +74 -0
- data/skills/dhanhq-ruby/references/market_data.md +3 -0
- data/skills/dhanhq-ruby/references/orders.md +7 -0
- metadata +59 -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,35 @@ 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/pipeline"
|
|
57
|
+
|
|
58
|
+
# Skills layer: multi-step trading workflows
|
|
59
|
+
require_relative "DhanHQ/skills"
|
|
60
|
+
require_relative "DhanHQ/skills/base"
|
|
61
|
+
require_relative "DhanHQ/skills/registry"
|
|
62
|
+
require_relative "DhanHQ/skills/workflow"
|
|
63
|
+
require_relative "DhanHQ/skills/builtin/buy_atm_call"
|
|
64
|
+
require_relative "DhanHQ/skills/builtin/square_off_all"
|
|
65
|
+
require_relative "DhanHQ/skills/builtin/square_off_position"
|
|
66
|
+
require_relative "DhanHQ/skills/builtin/iron_condor"
|
|
67
|
+
require_relative "DhanHQ/skills/builtin/strangle"
|
|
68
|
+
DhanHQ::Skills::Registry.load_builtins
|
|
69
|
+
|
|
45
70
|
class Error < StandardError; end
|
|
46
71
|
|
|
47
72
|
class << self
|
|
@@ -131,7 +156,7 @@ module DhanHQ
|
|
|
131
156
|
|
|
132
157
|
url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
|
|
133
158
|
conn = ::Faraday.new(url: url) do |c|
|
|
134
|
-
c.
|
|
159
|
+
c.request :url_encoded
|
|
135
160
|
c.adapter ::Faraday.default_adapter
|
|
136
161
|
end
|
|
137
162
|
|
|
@@ -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,74 @@
|
|
|
1
|
+
# DhanHQ Ruby SDK Skill
|
|
2
|
+
|
|
3
|
+
Use this skill when an agent needs to write, review, or operate Ruby code using the `DhanHQ` gem.
|
|
4
|
+
|
|
5
|
+
## Safety rules
|
|
6
|
+
|
|
7
|
+
- Prefer read-only tools and SDK calls unless the user explicitly asks to trade.
|
|
8
|
+
- Resolve instruments before trading with `DhanHQ::Models::Instrument.search` and use the returned `security_id` plus `exchange_segment`.
|
|
9
|
+
- Preview every order with `DhanHQ::Agent::OrderPreview` before any live order placement.
|
|
10
|
+
- Never place, modify, or cancel orders unless both `DHANHQ_MCP_ENABLE_WRITES=true` and `LIVE_TRADING=true` are set and the user has clearly confirmed the action.
|
|
11
|
+
- Include a `correlation_id` for every agent-originated order.
|
|
12
|
+
- Do not ask for or print access tokens.
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "dhan_hq"
|
|
18
|
+
require "dhan_hq/agent"
|
|
19
|
+
|
|
20
|
+
DhanHQ.configure_with_env
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Required environment variables:
|
|
24
|
+
|
|
25
|
+
- `DHAN_CLIENT_ID`
|
|
26
|
+
- `DHAN_ACCESS_TOKEN`
|
|
27
|
+
|
|
28
|
+
Optional agent variables:
|
|
29
|
+
|
|
30
|
+
- `DHANHQ_AGENT_SCOPES`, comma-separated scopes such as `portfolio:read,market:read,orders:read`
|
|
31
|
+
- `DHANHQ_MCP_ENABLE_WRITES=true` for agent write tools
|
|
32
|
+
- `LIVE_TRADING=true` for any live trading write
|
|
33
|
+
|
|
34
|
+
## Common calls
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
DhanHQ::Models::Profile.fetch
|
|
38
|
+
DhanHQ::Models::Funds.fetch
|
|
39
|
+
DhanHQ::Models::Holding.all
|
|
40
|
+
DhanHQ::Models::Position.all
|
|
41
|
+
DhanHQ::Models::Order.all
|
|
42
|
+
DhanHQ::Models::Trade.today
|
|
43
|
+
DhanHQ::Models::Instrument.search("RELIANCE", segments: ["NSE_EQ"], limit: 5)
|
|
44
|
+
DhanHQ::Models::MarketFeed.ltp("NSE_EQ" => ["2885"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Order preview
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
preview = DhanHQ::Agent::OrderPreview.new(
|
|
51
|
+
transaction_type: "BUY",
|
|
52
|
+
exchange_segment: "NSE_EQ",
|
|
53
|
+
product_type: "INTRADAY",
|
|
54
|
+
order_type: "MARKET",
|
|
55
|
+
validity: "DAY",
|
|
56
|
+
security_id: "2885",
|
|
57
|
+
quantity: 1,
|
|
58
|
+
correlation_id: "agent-20260702-001"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
preview.to_h
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## MCP server
|
|
65
|
+
|
|
66
|
+
The gem includes a stdio MCP executable:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
dhanhq-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Start with read-only scopes. Add write scopes only for explicitly confirmed trading sessions.
|
|
73
|
+
|
|
74
|
+
See the references directory for focused workflows.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Orders with agents
|
|
2
|
+
|
|
3
|
+
1. Search or otherwise verify the instrument.
|
|
4
|
+
2. Build order params using Dhan constants and API docs.
|
|
5
|
+
3. Run `DhanHQ::Agent::OrderPreview.new(params).to_h`.
|
|
6
|
+
4. Ask the user to confirm the preview.
|
|
7
|
+
5. Place only if `DHANHQ_MCP_ENABLE_WRITES=true`, `LIVE_TRADING=true`, and `orders:write` scope are present.
|