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,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a protective put strategy (buy stock, buy OTM put).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain →
|
|
9
|
+
# select OTM put → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("protective_put",
|
|
13
|
+
# symbol: "RELIANCE",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 100
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class ProtectivePut < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a protective put: buy the underlying equity, buy an OTM put as downside insurance."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 100
|
|
26
|
+
param :strike_offset, type: :number, default: 2.0
|
|
27
|
+
param :max_premium_pct, type: :number, default: 3.0
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_otm_put, priority: 4
|
|
33
|
+
step :build_intent, priority: 5
|
|
34
|
+
|
|
35
|
+
def find_instrument(ctx)
|
|
36
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::NSE_EQ, ctx[:symbol])
|
|
37
|
+
ctx
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_spot_price(ctx)
|
|
41
|
+
ctx[:spot_price] = ctx[:instrument].ltp
|
|
42
|
+
ctx
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_option_chain(ctx)
|
|
46
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
47
|
+
ctx
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select_otm_put(ctx)
|
|
51
|
+
spot = ctx[:spot_price].to_f
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
offset_pct = ctx[:strike_offset] / 100.0
|
|
54
|
+
max_prem = ctx[:max_premium_pct] / 100.0
|
|
55
|
+
|
|
56
|
+
target_strike = spot * (1 - offset_pct)
|
|
57
|
+
otm_put = nearest_strike(chain, target_strike)
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "Could not find suitable OTM put strike near #{target_strike}" unless otm_put
|
|
60
|
+
|
|
61
|
+
premium = leg_premium(otm_put, "PE").to_f
|
|
62
|
+
premium_pct = premium / spot
|
|
63
|
+
|
|
64
|
+
raise ArgumentError, "Put premium #{premium_pct * 100}% exceeds max #{ctx[:max_premium_pct]}%" if premium_pct > max_prem
|
|
65
|
+
|
|
66
|
+
ctx[:put_strike] = otm_put[:strike]
|
|
67
|
+
ctx[:put_security_id] = leg_security_id(otm_put, "PE")
|
|
68
|
+
ctx[:put_premium] = premium
|
|
69
|
+
ctx[:equity_security_id] = ctx[:instrument].security_id
|
|
70
|
+
ctx
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_intent(ctx)
|
|
74
|
+
ctx[:intent] = {
|
|
75
|
+
trade_type: "PROTECTIVE_PUT",
|
|
76
|
+
symbol: ctx[:symbol],
|
|
77
|
+
quantity: ctx[:quantity],
|
|
78
|
+
legs: [
|
|
79
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, instrument_type: DhanHQ::Constants::InstrumentType::EQUITY, security_id: ctx[:equity_security_id],
|
|
80
|
+
quantity: ctx[:quantity] },
|
|
81
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: ctx[:put_strike], security_id: ctx[:put_security_id], quantity: ctx[:quantity], premium: ctx[:put_premium] }
|
|
82
|
+
],
|
|
83
|
+
note: "Protective put prepared: Buy #{ctx[:quantity]} #{ctx[:symbol]}, Buy #{ctx[:quantity]} #{ctx[:put_strike]} PE. Await human confirmation."
|
|
84
|
+
}
|
|
85
|
+
ctx
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
risk "destructive_write"
|
|
16
|
+
scope "orders:write"
|
|
17
|
+
description "Exit all open positions at market price."
|
|
18
|
+
|
|
19
|
+
step :fetch_positions, priority: 1
|
|
20
|
+
step :exit_positions, priority: 2
|
|
21
|
+
|
|
22
|
+
def fetch_positions(ctx)
|
|
23
|
+
ctx[:positions] = DhanHQ::Models::Position.all.reject { |p| p.net_qty.to_i.zero? }
|
|
24
|
+
ctx
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def exit_positions(ctx)
|
|
28
|
+
results = ctx[:positions].map do
|
|
29
|
+
DhanHQ::Models::Position.exit_all!
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
{ error: e.message }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
ctx[:exit_results] = results
|
|
35
|
+
ctx[:exited_count] = results.count { |r| !(r.is_a?(Hash) && r.key?(:error)) }
|
|
36
|
+
ctx[:failed_count] = results.count { |r| r.is_a?(Hash) && r.key?(:error) }
|
|
37
|
+
ctx
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
risk "destructive_write"
|
|
18
|
+
scope "orders:write"
|
|
19
|
+
description "Exit a specific open position by symbol and exchange segment."
|
|
20
|
+
|
|
21
|
+
param :symbol, type: :string, required: true
|
|
22
|
+
param :exchange_segment, type: :string, required: true
|
|
23
|
+
|
|
24
|
+
step :find_position, priority: 1
|
|
25
|
+
step :exit_position, priority: 2
|
|
26
|
+
|
|
27
|
+
def find_position(ctx)
|
|
28
|
+
positions = DhanHQ::Models::Position.all
|
|
29
|
+
target = positions.find do |p|
|
|
30
|
+
p.exchange_segment.to_s == ctx[:exchange_segment].to_s && p.trading_symbol.to_s.upcase == ctx[:symbol].to_s.upcase
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
raise ArgumentError, "No open position found for #{ctx[:symbol]} on #{ctx[:exchange_segment]}" unless target
|
|
34
|
+
|
|
35
|
+
ctx[:position] = target
|
|
36
|
+
ctx[:security_id] = target.security_id
|
|
37
|
+
ctx[:trading_symbol] = target.trading_symbol
|
|
38
|
+
ctx[:net_quantity] = target.net_qty.to_i
|
|
39
|
+
ctx
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exit_position(ctx)
|
|
43
|
+
ctx[:exit_result] = DhanHQ::Models::Position.exit_all!
|
|
44
|
+
ctx[:exited] = true
|
|
45
|
+
ctx
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a long straddle (buy ATM call + buy ATM put).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain →
|
|
9
|
+
# select ATM strikes → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("straddle",
|
|
13
|
+
# symbol: "NIFTY",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 25
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class Straddle < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a long straddle: buy ATM call + buy ATM put at the same strike."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 25
|
|
26
|
+
param :stop_loss, type: :number, default: 300
|
|
27
|
+
param :target, type: :number, default: 600
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_atm_strikes, priority: 4
|
|
33
|
+
step :build_intent, priority: 5
|
|
34
|
+
|
|
35
|
+
def find_instrument(ctx)
|
|
36
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
37
|
+
ctx
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def get_spot_price(ctx)
|
|
41
|
+
ctx[:spot_price] = ctx[:instrument].ltp
|
|
42
|
+
ctx
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_option_chain(ctx)
|
|
46
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
47
|
+
ctx
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def select_atm_strikes(ctx)
|
|
51
|
+
spot = ctx[:spot_price].to_f
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
|
|
54
|
+
atm = nearest_strike(chain, spot)
|
|
55
|
+
raise ArgumentError, "Could not find ATM strike" unless atm
|
|
56
|
+
|
|
57
|
+
ctx[:atm_strike] = atm[:strike]
|
|
58
|
+
ctx[:ce_security_id] = leg_security_id(atm, "CE")
|
|
59
|
+
ctx[:pe_security_id] = leg_security_id(atm, "PE")
|
|
60
|
+
ctx[:ce_premium] = leg_premium(atm, "CE")
|
|
61
|
+
ctx[:pe_premium] = leg_premium(atm, "PE")
|
|
62
|
+
ctx[:total_premium] = ctx[:ce_premium].to_f + ctx[:pe_premium].to_f
|
|
63
|
+
ctx
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build_intent(ctx)
|
|
67
|
+
ctx[:intent] = {
|
|
68
|
+
trade_type: "STRADDLE",
|
|
69
|
+
symbol: ctx[:symbol],
|
|
70
|
+
expiry: ctx[:expiry],
|
|
71
|
+
quantity: ctx[:quantity],
|
|
72
|
+
legs: [
|
|
73
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: ctx[:atm_strike], security_id: ctx[:ce_security_id], premium: ctx[:ce_premium] },
|
|
74
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: ctx[:atm_strike], security_id: ctx[:pe_security_id], premium: ctx[:pe_premium] }
|
|
75
|
+
],
|
|
76
|
+
total_premium: ctx[:total_premium],
|
|
77
|
+
break_even_upside: ctx[:atm_strike].to_f + ctx[:total_premium],
|
|
78
|
+
break_even_downside: ctx[:atm_strike].to_f - ctx[:total_premium],
|
|
79
|
+
stop_loss: ctx[:stop_loss],
|
|
80
|
+
target: ctx[:target],
|
|
81
|
+
note: "Long straddle prepared. Await human confirmation."
|
|
82
|
+
}
|
|
83
|
+
ctx
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
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
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a long strangle: buy OTM call + buy OTM put around the current spot price."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 50
|
|
26
|
+
param :offset_pct, type: :number, default: 1.0
|
|
27
|
+
param :stop_loss, type: :number, default: 200
|
|
28
|
+
param :target, type: :number, default: 400
|
|
29
|
+
|
|
30
|
+
step :find_instrument, priority: 1
|
|
31
|
+
step :get_spot_price, priority: 2
|
|
32
|
+
step :get_option_chain, priority: 3
|
|
33
|
+
step :select_strikes, priority: 4
|
|
34
|
+
step :build_intent, priority: 5
|
|
35
|
+
|
|
36
|
+
def find_instrument(ctx)
|
|
37
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, ctx[:symbol])
|
|
38
|
+
ctx
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def get_spot_price(ctx)
|
|
42
|
+
ctx[:spot_price] = ctx[:instrument].ltp
|
|
43
|
+
ctx
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def get_option_chain(ctx)
|
|
47
|
+
ctx[:chain] = ctx[:instrument].option_chain(expiry: ctx[:expiry])
|
|
48
|
+
ctx
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def select_strikes(ctx)
|
|
52
|
+
spot = ctx[:spot_price].to_f
|
|
53
|
+
chain = ctx[:chain]
|
|
54
|
+
offset = ctx[:offset_pct] / 100.0
|
|
55
|
+
|
|
56
|
+
ce_strike = spot * (1 + offset)
|
|
57
|
+
pe_strike = spot * (1 - offset)
|
|
58
|
+
|
|
59
|
+
long_ce = nearest_strike(chain, ce_strike)
|
|
60
|
+
long_pe = nearest_strike(chain, pe_strike)
|
|
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]
|
|
66
|
+
ctx[:pe_strike] = long_pe[:strike]
|
|
67
|
+
ctx[:ce_security_id] = leg_security_id(long_ce, "CE")
|
|
68
|
+
ctx[:pe_security_id] = leg_security_id(long_pe, "PE")
|
|
69
|
+
ctx[:ce_premium] = leg_premium(long_ce, "CE")
|
|
70
|
+
ctx[:pe_premium] = leg_premium(long_pe, "PE")
|
|
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
|