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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
# Base class for all trading skills.
|
|
6
|
+
#
|
|
7
|
+
# Provides a DSL for defining parameters, steps, and execution logic.
|
|
8
|
+
# Skills are stateless — context is passed through and returned.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class BuyAtmCall < DhanHQ::Skills::Base
|
|
12
|
+
# param :symbol, type: :string, required: true
|
|
13
|
+
# param :expiry, type: :string, required: true
|
|
14
|
+
# param :quantity, type: :integer, default: 50
|
|
15
|
+
#
|
|
16
|
+
# step :find_instrument
|
|
17
|
+
# step :get_spot_price
|
|
18
|
+
# step :prepare_intent
|
|
19
|
+
#
|
|
20
|
+
# def find_instrument(ctx)
|
|
21
|
+
# ctx[:instrument] = DhanHQ::Models::Instrument.find("IDX_I", ctx[:symbol])
|
|
22
|
+
# ctx
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# def get_spot_price(ctx)
|
|
26
|
+
# ctx[:spot_price] = ctx[:instrument].ltp[:ltp]
|
|
27
|
+
# ctx
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# def prepare_intent(ctx)
|
|
31
|
+
# ctx[:intent] = { symbol: ctx[:symbol], spot: ctx[:spot_price] }
|
|
32
|
+
# ctx
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
class Base
|
|
37
|
+
class << self
|
|
38
|
+
# Define a parameter for this skill.
|
|
39
|
+
#
|
|
40
|
+
# @param name [Symbol] parameter name
|
|
41
|
+
# @param type [Symbol] :string, :integer, :number, :boolean
|
|
42
|
+
# @param required [Boolean] whether the parameter is required
|
|
43
|
+
# @param default [Object] default value if not provided
|
|
44
|
+
def param(name, type: :string, required: false, default: nil, description: nil)
|
|
45
|
+
@params ||= {}
|
|
46
|
+
@params[name] = { type: type, required: required, default: default, description: description }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Define a step in the skill execution sequence.
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] method name to call
|
|
52
|
+
# @param priority [Integer] execution order (lower = earlier)
|
|
53
|
+
def step(name, priority: 10)
|
|
54
|
+
@steps ||= []
|
|
55
|
+
@steps << { name: name, priority: priority }
|
|
56
|
+
@steps.sort_by! { |s| s[:priority] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Accessor for defined parameters.
|
|
60
|
+
def params
|
|
61
|
+
@params || {}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Accessor for defined steps.
|
|
65
|
+
def steps
|
|
66
|
+
@steps || []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Validate that all required parameters are present.
|
|
70
|
+
#
|
|
71
|
+
# @param args [Hash] provided parameters
|
|
72
|
+
# @raise [ArgumentError] if required parameters are missing
|
|
73
|
+
def validate_params!(args)
|
|
74
|
+
params.each do |name, config|
|
|
75
|
+
next unless config[:required]
|
|
76
|
+
|
|
77
|
+
value = args[name] || args[name.to_s]
|
|
78
|
+
next unless value.nil?
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "Missing required parameter: #{name}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Execute the skill with the given arguments.
|
|
86
|
+
#
|
|
87
|
+
# @param args [Hash] skill parameters (symbol or string keys)
|
|
88
|
+
# @return [Hash] context with all accumulated state
|
|
89
|
+
# @raise [ArgumentError] if required parameters are missing
|
|
90
|
+
def call(args = {})
|
|
91
|
+
ctx = build_context(args)
|
|
92
|
+
self.class.validate_params!(ctx)
|
|
93
|
+
|
|
94
|
+
self.class.steps.each do |step|
|
|
95
|
+
result = send(step[:name], ctx)
|
|
96
|
+
ctx = result if result.is_a?(Hash)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
ctx
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Skill name (defaults to class name).
|
|
103
|
+
def name
|
|
104
|
+
self.class.name || self.class.to_s
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Skill description (override in subclasses).
|
|
108
|
+
def description
|
|
109
|
+
self.class.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# List of parameter definitions for this skill.
|
|
113
|
+
def param_definitions
|
|
114
|
+
self.class.params
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def build_context(args)
|
|
120
|
+
ctx = {}
|
|
121
|
+
|
|
122
|
+
self.class.params.each do |name, config|
|
|
123
|
+
value = args[name] || args[name.to_s]
|
|
124
|
+
value = config[:default] if value.nil? && config[:default]
|
|
125
|
+
ctx[name] = value
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
ctx
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to buy an ATM call option on an index.
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → get spot price → get option chain →
|
|
9
|
+
# select ATM strike → prepare trade intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("buy_atm_call",
|
|
13
|
+
# symbol: "NIFTY",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 50
|
|
16
|
+
# )
|
|
17
|
+
# puts result[:intent]
|
|
18
|
+
#
|
|
19
|
+
class BuyAtmCall < Base
|
|
20
|
+
param :symbol, type: :string, required: true
|
|
21
|
+
param :expiry, type: :string, required: true
|
|
22
|
+
param :quantity, type: :integer, default: 50
|
|
23
|
+
param :stop_loss, type: :number, default: 100
|
|
24
|
+
param :target, type: :number, default: 200
|
|
25
|
+
|
|
26
|
+
step :find_instrument, priority: 1
|
|
27
|
+
step :get_spot_price, priority: 2
|
|
28
|
+
step :get_option_chain, priority: 3
|
|
29
|
+
step :select_atm_strike, priority: 4
|
|
30
|
+
step :prepare_intent, priority: 5
|
|
31
|
+
|
|
32
|
+
def find_instrument(ctx)
|
|
33
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
34
|
+
ctx
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_spot_price(ctx)
|
|
38
|
+
ltp = ctx[:instrument].ltp
|
|
39
|
+
ctx[:spot_price] = ltp[:ltp] || ltp["ltp"]
|
|
40
|
+
ctx
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_option_chain(ctx)
|
|
44
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
45
|
+
ctx
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def select_atm_strike(ctx)
|
|
49
|
+
spot = ctx[:spot_price]
|
|
50
|
+
chain = ctx[:chain]
|
|
51
|
+
|
|
52
|
+
ce_options = chain.select do |opt|
|
|
53
|
+
opt[:option_type] == "CE" || opt["optionType"] == "CE"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
atm = ce_options.min_by do |opt|
|
|
57
|
+
strike = opt[:strike] || opt["strike"]
|
|
58
|
+
(strike.to_f - spot).abs
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
ctx[:selected_option] = atm
|
|
62
|
+
ctx[:security_id] = atm[:security_id] || atm["securityId"]
|
|
63
|
+
ctx[:strike] = atm[:strike] || atm["strike"]
|
|
64
|
+
ctx[:premium] = atm[:last_price] || atm["lastPrice"] || atm[:ltp] || atm["ltp"]
|
|
65
|
+
ctx
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def prepare_intent(ctx)
|
|
69
|
+
ctx[:intent] = {
|
|
70
|
+
trade_type: "OPTIONS_BUY",
|
|
71
|
+
instrument: "#{ctx[:symbol]} #{ctx[:strike]} CE",
|
|
72
|
+
security_id: ctx[:security_id],
|
|
73
|
+
strike: ctx[:strike],
|
|
74
|
+
expiry: ctx[:expiry],
|
|
75
|
+
option_type: "CE",
|
|
76
|
+
quantity: ctx[:quantity],
|
|
77
|
+
premium: ctx[:premium],
|
|
78
|
+
stop_loss: ctx[:stop_loss],
|
|
79
|
+
target: ctx[:target],
|
|
80
|
+
note: "Prepared ATM call buy. Await human confirmation."
|
|
81
|
+
}
|
|
82
|
+
ctx
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build an iron condor strategy (sell OTM CE + PE, buy further OTM CE + PE).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain → select strikes → build intent.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = DhanHQ::Skills::Registry.call("iron_condor",
|
|
12
|
+
# symbol: "NIFTY",
|
|
13
|
+
# expiry: "2026-01-30",
|
|
14
|
+
# quantity: 50,
|
|
15
|
+
# wing_width: 200
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class IronCondor < Base
|
|
19
|
+
param :symbol, type: :string, required: true
|
|
20
|
+
param :expiry, type: :string, required: true
|
|
21
|
+
param :quantity, type: :integer, default: 50
|
|
22
|
+
param :wing_width, type: :number, default: 200
|
|
23
|
+
param :max_loss, type: :number, default: 5000
|
|
24
|
+
|
|
25
|
+
step :find_instrument, priority: 1
|
|
26
|
+
step :get_spot_price, priority: 2
|
|
27
|
+
step :get_option_chain, priority: 3
|
|
28
|
+
step :select_strikes, priority: 4
|
|
29
|
+
step :build_intent, priority: 5
|
|
30
|
+
|
|
31
|
+
def find_instrument(ctx)
|
|
32
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
33
|
+
ctx
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def get_spot_price(ctx)
|
|
37
|
+
ltp = ctx[:instrument].ltp
|
|
38
|
+
ctx[:spot_price] = ltp[:ltp] || ltp["ltp"]
|
|
39
|
+
ctx
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_option_chain(ctx)
|
|
43
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
44
|
+
ctx
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def select_strikes(ctx)
|
|
48
|
+
spot = ctx[:spot_price]
|
|
49
|
+
chain = ctx[:chain]
|
|
50
|
+
wing = ctx[:wing_width]
|
|
51
|
+
|
|
52
|
+
ce_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "CE" }
|
|
53
|
+
.sort_by { |o| (o[:strike] || o["strike"]).to_f }
|
|
54
|
+
pe_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "PE" }
|
|
55
|
+
.sort_by { |o| (o[:strike] || o["strike"]).to_f }
|
|
56
|
+
|
|
57
|
+
atm = ce_options.min_by { |o| (o[:strike] || o["strike"]).to_f - spot.to_f }
|
|
58
|
+
|
|
59
|
+
atm_strike = (atm[:strike] || atm["strike"]).to_f
|
|
60
|
+
|
|
61
|
+
short_ce = ce_options.find { |o| ((o[:strike] || o["strike"]).to_f - (atm_strike + wing)).abs < 0.001 }
|
|
62
|
+
long_ce = ce_options.find { |o| ((o[:strike] || o["strike"]).to_f - (atm_strike + (wing * 2))).abs < 0.001 }
|
|
63
|
+
short_pe = pe_options.find { |o| ((o[:strike] || o["strike"]).to_f - (atm_strike - wing)).abs < 0.001 }
|
|
64
|
+
long_pe = pe_options.find { |o| ((o[:strike] || o["strike"]).to_f - (atm_strike - (wing * 2))).abs < 0.001 }
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "Could not build iron condor — insufficient strikes in chain" unless short_ce && long_ce && short_pe && long_pe
|
|
67
|
+
|
|
68
|
+
ctx[:legs] = [
|
|
69
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "CE", strike: atm_strike + wing, security_id: short_ce[:security_id] || short_ce["securityId"] },
|
|
70
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: atm_strike + (wing * 2), security_id: long_ce[:security_id] || long_ce["securityId"] },
|
|
71
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "PE", strike: atm_strike - wing, security_id: short_pe[:security_id] || short_pe["securityId"] },
|
|
72
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: atm_strike - (wing * 2), security_id: long_pe[:security_id] || long_pe["securityId"] }
|
|
73
|
+
]
|
|
74
|
+
ctx
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_intent(ctx)
|
|
78
|
+
ctx[:intent] = {
|
|
79
|
+
trade_type: "IRON_CONDOR",
|
|
80
|
+
symbol: ctx[:symbol],
|
|
81
|
+
expiry: ctx[:expiry],
|
|
82
|
+
quantity: ctx[:quantity],
|
|
83
|
+
wing_width: ctx[:wing_width],
|
|
84
|
+
max_loss: ctx[:max_loss],
|
|
85
|
+
legs: ctx[:legs],
|
|
86
|
+
note: "Iron condor prepared. Await human confirmation before execution."
|
|
87
|
+
}
|
|
88
|
+
ctx
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to exit all open positions at market price.
|
|
7
|
+
#
|
|
8
|
+
# Steps: fetch positions → exit each position → summarize results.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = DhanHQ::Skills::Registry.call("square_off_all")
|
|
12
|
+
# puts result[:exited_count]
|
|
13
|
+
#
|
|
14
|
+
class SquareOffAll < Base
|
|
15
|
+
step :fetch_positions, priority: 1
|
|
16
|
+
step :exit_positions, priority: 2
|
|
17
|
+
|
|
18
|
+
def fetch_positions(ctx)
|
|
19
|
+
ctx[:positions] = DhanHQ::Models::Position.all.reject do |p|
|
|
20
|
+
qty = begin
|
|
21
|
+
p[:net_quantity] || p["netQuantity"] || p.net_quantity
|
|
22
|
+
rescue StandardError
|
|
23
|
+
0
|
|
24
|
+
end
|
|
25
|
+
qty.to_i.zero?
|
|
26
|
+
end
|
|
27
|
+
ctx
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exit_positions(ctx)
|
|
31
|
+
results = ctx[:positions].map do
|
|
32
|
+
DhanHQ::Models::Position.exit_all!
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
{ error: e.message }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ctx[:exit_results] = results
|
|
38
|
+
ctx[:exited_count] = results.count { |r| !(r.is_a?(Hash) && r.key?(:error)) }
|
|
39
|
+
ctx[:failed_count] = results.count { |r| r.is_a?(Hash) && r.key?(:error) }
|
|
40
|
+
ctx
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to exit a specific position by symbol.
|
|
7
|
+
#
|
|
8
|
+
# Steps: find position → exit position → return result.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = DhanHQ::Skills::Registry.call("square_off_position",
|
|
12
|
+
# symbol: "NIFTY",
|
|
13
|
+
# exchange_segment: "IDX_I"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
class SquareOffPosition < Base
|
|
17
|
+
param :symbol, type: :string, required: true
|
|
18
|
+
param :exchange_segment, type: :string, required: true
|
|
19
|
+
|
|
20
|
+
step :find_position, priority: 1
|
|
21
|
+
step :exit_position, priority: 2
|
|
22
|
+
|
|
23
|
+
def find_position(ctx)
|
|
24
|
+
positions = DhanHQ::Models::Position.all
|
|
25
|
+
target = positions.find do |p|
|
|
26
|
+
seg = p[:exchange_segment] || p["exchange_segment"]
|
|
27
|
+
sym = p[:trading_symbol] || p["tradingSymbol"] || p[:symbol] || p["symbol"]
|
|
28
|
+
seg.to_s == ctx[:exchange_segment].to_s && sym.to_s.upcase == ctx[:symbol].to_s.upcase
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, "No open position found for #{ctx[:symbol]} on #{ctx[:exchange_segment]}" unless target
|
|
32
|
+
|
|
33
|
+
ctx[:position] = target
|
|
34
|
+
ctx[:security_id] = target[:security_id] || target["securityId"]
|
|
35
|
+
ctx[:trading_symbol] = target[:trading_symbol] || target["tradingSymbol"]
|
|
36
|
+
ctx[:net_quantity] = (target[:net_quantity] || target["netQuantity"] || target.net_quantity).to_i
|
|
37
|
+
ctx
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exit_position(ctx)
|
|
41
|
+
ctx[:exit_result] = DhanHQ::Models::Position.exit_all!
|
|
42
|
+
ctx[:exited] = true
|
|
43
|
+
ctx
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a long strangle (buy OTM CE + buy OTM PE).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain → select strikes → build intent.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# result = DhanHQ::Skills::Registry.call("strangle",
|
|
12
|
+
# symbol: "NIFTY",
|
|
13
|
+
# expiry: "2026-01-30",
|
|
14
|
+
# quantity: 50,
|
|
15
|
+
# offset_pct: 1.0
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Strangle < Base
|
|
19
|
+
param :symbol, type: :string, required: true
|
|
20
|
+
param :expiry, type: :string, required: true
|
|
21
|
+
param :quantity, type: :integer, default: 50
|
|
22
|
+
param :offset_pct, type: :number, default: 1.0
|
|
23
|
+
param :stop_loss, type: :number, default: 200
|
|
24
|
+
param :target, type: :number, default: 400
|
|
25
|
+
|
|
26
|
+
step :find_instrument, priority: 1
|
|
27
|
+
step :get_spot_price, priority: 2
|
|
28
|
+
step :get_option_chain, priority: 3
|
|
29
|
+
step :select_strikes, priority: 4
|
|
30
|
+
step :build_intent, priority: 5
|
|
31
|
+
|
|
32
|
+
def find_instrument(ctx)
|
|
33
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
34
|
+
ctx
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_spot_price(ctx)
|
|
38
|
+
ltp = ctx[:instrument].ltp
|
|
39
|
+
ctx[:spot_price] = ltp[:ltp] || ltp["ltp"]
|
|
40
|
+
ctx
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_option_chain(ctx)
|
|
44
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
45
|
+
ctx
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def select_strikes(ctx)
|
|
49
|
+
spot = ctx[:spot_price].to_f
|
|
50
|
+
chain = ctx[:chain]
|
|
51
|
+
offset = ctx[:offset_pct] / 100.0
|
|
52
|
+
|
|
53
|
+
ce_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "CE" }
|
|
54
|
+
pe_options = chain.select { |o| (o[:option_type] || o["optionType"]) == "PE" }
|
|
55
|
+
|
|
56
|
+
ce_strike = spot * (1 + offset)
|
|
57
|
+
pe_strike = spot * (1 - offset)
|
|
58
|
+
|
|
59
|
+
long_ce = ce_options.min_by { |o| ((o[:strike] || o["strike"]).to_f - ce_strike).abs }
|
|
60
|
+
long_pe = pe_options.min_by { |o| ((o[:strike] || o["strike"]).to_f - pe_strike).abs }
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "Could not find suitable CE strike near #{ce_strike}" unless long_ce
|
|
63
|
+
raise ArgumentError, "Could not find suitable PE strike near #{pe_strike}" unless long_pe
|
|
64
|
+
|
|
65
|
+
ctx[:ce_strike] = long_ce[:strike] || long_ce["strike"]
|
|
66
|
+
ctx[:pe_strike] = long_pe[:strike] || long_pe["strike"]
|
|
67
|
+
ctx[:ce_security_id] = long_ce[:security_id] || long_ce["securityId"]
|
|
68
|
+
ctx[:pe_security_id] = long_pe[:security_id] || long_pe["securityId"]
|
|
69
|
+
ctx[:ce_premium] = long_ce[:last_price] || long_ce["lastPrice"] || long_ce[:ltp]
|
|
70
|
+
ctx[:pe_premium] = long_pe[:last_price] || long_pe["lastPrice"] || long_pe[:ltp]
|
|
71
|
+
ctx
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_intent(ctx)
|
|
75
|
+
ctx[:intent] = {
|
|
76
|
+
trade_type: "STRANGLE",
|
|
77
|
+
symbol: ctx[:symbol],
|
|
78
|
+
expiry: ctx[:expiry],
|
|
79
|
+
quantity: ctx[:quantity],
|
|
80
|
+
legs: [
|
|
81
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: ctx[:ce_strike], security_id: ctx[:ce_security_id], premium: ctx[:ce_premium] },
|
|
82
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: ctx[:pe_strike], security_id: ctx[:pe_security_id], premium: ctx[:pe_premium] }
|
|
83
|
+
],
|
|
84
|
+
stop_loss: ctx[:stop_loss],
|
|
85
|
+
target: ctx[:target],
|
|
86
|
+
note: "Long strangle prepared. Await human confirmation."
|
|
87
|
+
}
|
|
88
|
+
ctx
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
# Registry for skill definitions.
|
|
6
|
+
#
|
|
7
|
+
# Maps skill names to their classes. Skills are registered explicitly
|
|
8
|
+
# or discovered via conventions.
|
|
9
|
+
#
|
|
10
|
+
# @example Register a skill
|
|
11
|
+
# DhanHQ::Skills::Registry.register("buy_atm_call", BuyAtmCall)
|
|
12
|
+
#
|
|
13
|
+
# @example Find a skill
|
|
14
|
+
# skill = DhanHQ::Skills::Registry.find("buy_atm_call")
|
|
15
|
+
# result = skill.call(symbol: "NIFTY", expiry: "2026-01-30")
|
|
16
|
+
#
|
|
17
|
+
class Registry
|
|
18
|
+
class << self
|
|
19
|
+
# Register a skill class by name.
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] skill name
|
|
22
|
+
# @param klass [Class] skill class inheriting from Base
|
|
23
|
+
# @raise [ArgumentError] if the class is not a Base subclass
|
|
24
|
+
def register(name, klass)
|
|
25
|
+
raise ArgumentError, "Skill class must inherit from DhanHQ::Skills::Base" unless klass < Base
|
|
26
|
+
|
|
27
|
+
skills[name.to_s] = klass
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find a skill by name.
|
|
31
|
+
#
|
|
32
|
+
# @param name [String] skill name
|
|
33
|
+
# @return [Class] the skill class
|
|
34
|
+
# @raise [KeyError] if skill is not found
|
|
35
|
+
def find(name)
|
|
36
|
+
skills.fetch(name.to_s) { raise KeyError, "Unknown skill: #{name}" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Execute a skill by name with the given arguments.
|
|
40
|
+
#
|
|
41
|
+
# @param name [String] skill name
|
|
42
|
+
# @param args [Hash] skill parameters
|
|
43
|
+
# @return [Hash] context with all accumulated state
|
|
44
|
+
def call(name, args = {})
|
|
45
|
+
find(name).new.call(args)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# List all registered skill names.
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<String>]
|
|
51
|
+
def names
|
|
52
|
+
skills.keys.sort
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# List all registered skills with metadata.
|
|
56
|
+
#
|
|
57
|
+
# @return [Array<Hash>]
|
|
58
|
+
def list
|
|
59
|
+
skills.map do |name, klass|
|
|
60
|
+
instance = klass.new
|
|
61
|
+
{
|
|
62
|
+
name: name,
|
|
63
|
+
description: instance.description,
|
|
64
|
+
params: instance.param_definitions,
|
|
65
|
+
steps: klass.steps.map { |s| s[:name] }
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Clear all registered skills (for testing).
|
|
71
|
+
def clear!
|
|
72
|
+
@skills = {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Load built-in skills from the builtin directory and register them.
|
|
76
|
+
def load_builtins
|
|
77
|
+
Dir[File.join(__dir__, "builtin", "*.rb")].each do |file|
|
|
78
|
+
require file
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Auto-register any Builtin classes that inherit from Base
|
|
82
|
+
DhanHQ::Skills::Builtin.constants.each do |const_name|
|
|
83
|
+
klass = DhanHQ::Skills::Builtin.const_get(const_name)
|
|
84
|
+
next unless klass.is_a?(Class) && klass < Base
|
|
85
|
+
|
|
86
|
+
name = const_name.to_s
|
|
87
|
+
.gsub(/([a-z])([A-Z])/, '\1_\2')
|
|
88
|
+
.downcase
|
|
89
|
+
register(name, klass) unless skills.key?(name)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def skills
|
|
96
|
+
@skills ||= {}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
# Workflow orchestrates multi-step skill execution with branching and error handling.
|
|
6
|
+
#
|
|
7
|
+
# A workflow is a sequence of steps that can succeed, fail, or branch
|
|
8
|
+
# based on the result of each step.
|
|
9
|
+
#
|
|
10
|
+
# @example Define a workflow
|
|
11
|
+
# workflow = DhanHQ::Skills::Workflow.new do
|
|
12
|
+
# step :check_funds do |ctx|
|
|
13
|
+
# funds = DhanHQ::Models::Funds.fetch
|
|
14
|
+
# ctx[:available_balance] = funds[:available_balance]
|
|
15
|
+
# ctx
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# step :validate_margin do |ctx|
|
|
19
|
+
# raise "Insufficient margin" if ctx[:available_balance] < 10_000
|
|
20
|
+
# ctx
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# step :place_order do |ctx|
|
|
24
|
+
# ctx[:order] = DhanHQ::Models::Order.place(ctx[:order_params])
|
|
25
|
+
# ctx
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# result = workflow.call(order_params: { ... })
|
|
30
|
+
#
|
|
31
|
+
class Workflow
|
|
32
|
+
Step = Struct.new(:name, :block, :priority)
|
|
33
|
+
|
|
34
|
+
attr_reader :name, :steps
|
|
35
|
+
|
|
36
|
+
def initialize(name: "workflow", &block)
|
|
37
|
+
@name = name
|
|
38
|
+
@steps = []
|
|
39
|
+
instance_eval(&block) if block
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Define a step in the workflow.
|
|
43
|
+
#
|
|
44
|
+
# @param name [Symbol] step name
|
|
45
|
+
# @param priority [Integer] execution order (lower = earlier)
|
|
46
|
+
# @param block [Proc] step implementation
|
|
47
|
+
def step(name, priority: 10, &block)
|
|
48
|
+
@steps << Step.new(name, block, priority)
|
|
49
|
+
@steps.sort_by!(&:priority)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Execute the workflow with the given context.
|
|
53
|
+
#
|
|
54
|
+
# @param ctx [Hash] initial context
|
|
55
|
+
# @return [Hash] final context after all steps
|
|
56
|
+
# @raise [RuntimeError] if any step fails
|
|
57
|
+
def call(ctx = {})
|
|
58
|
+
@steps.each do |step|
|
|
59
|
+
result = step.block.call(ctx)
|
|
60
|
+
ctx = result if result.is_a?(Hash)
|
|
61
|
+
end
|
|
62
|
+
ctx
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Multi-step trading workflows built on top of MCP tools.
|
|
5
|
+
#
|
|
6
|
+
# Skills compose individual tool calls into reusable trading strategies.
|
|
7
|
+
# Each skill defines a sequence of steps that execute in order, with
|
|
8
|
+
# context passed between steps.
|
|
9
|
+
#
|
|
10
|
+
# @example Define a custom skill
|
|
11
|
+
# class MySkill < DhanHQ::Skills::Base
|
|
12
|
+
# param :symbol, type: :string, required: true
|
|
13
|
+
# param :quantity, type: :integer, default: 1
|
|
14
|
+
#
|
|
15
|
+
# step :find_instrument
|
|
16
|
+
# step :place_order
|
|
17
|
+
#
|
|
18
|
+
# def find_instrument(ctx)
|
|
19
|
+
# ctx[:instrument] = DhanHQ::Models::Instrument.find("NSE_EQ", ctx[:symbol])
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# def place_order(ctx)
|
|
23
|
+
# DhanHQ::Models::Order.place(security_id: ctx[:instrument].security_id, quantity: ctx[:quantity])
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
module Skills
|
|
28
|
+
end
|
|
29
|
+
end
|