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,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a bear call spread (sell OTM call, buy further OTM call).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain →
|
|
9
|
+
# select strikes → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("bear_call_spread",
|
|
13
|
+
# symbol: "NIFTY",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 50
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class BearCallSpread < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a bear call spread: sell an OTM call, buy a further OTM call for defined risk."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 50
|
|
26
|
+
param :spread_width, type: :number, default: 200
|
|
27
|
+
param :max_loss, type: :number, default: 5000
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_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_strikes(ctx)
|
|
51
|
+
spot = ctx[:spot_price].to_f
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
spread = ctx[:spread_width].to_f
|
|
54
|
+
|
|
55
|
+
atm_strike_price = nearest_strike(chain, spot)[:strike].to_f
|
|
56
|
+
|
|
57
|
+
short_call = find_strike(chain, atm_strike_price + spread)
|
|
58
|
+
long_call = find_strike(chain, atm_strike_price + (spread * 2))
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "Could not build bear call spread — insufficient strikes in chain" unless short_call && long_call
|
|
61
|
+
|
|
62
|
+
ctx[:legs] = [
|
|
63
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "CE", strike: short_call[:strike],
|
|
64
|
+
security_id: leg_security_id(short_call, "CE") },
|
|
65
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: long_call[:strike],
|
|
66
|
+
security_id: leg_security_id(long_call, "CE") }
|
|
67
|
+
]
|
|
68
|
+
ctx
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_intent(ctx)
|
|
72
|
+
ctx[:intent] = {
|
|
73
|
+
trade_type: "BEAR_CALL_SPREAD",
|
|
74
|
+
symbol: ctx[:symbol],
|
|
75
|
+
expiry: ctx[:expiry],
|
|
76
|
+
quantity: ctx[:quantity],
|
|
77
|
+
spread_width: ctx[:spread_width],
|
|
78
|
+
max_loss: ctx[:max_loss],
|
|
79
|
+
legs: ctx[:legs],
|
|
80
|
+
note: "Bear call spread prepared. Await human confirmation before execution."
|
|
81
|
+
}
|
|
82
|
+
ctx
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a bull put spread (sell OTM put, buy further OTM put).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find instrument → spot price → option chain →
|
|
9
|
+
# select strikes → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("bull_put_spread",
|
|
13
|
+
# symbol: "NIFTY",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 50
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class BullPutSpread < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a bull put spread: sell an OTM put, buy a further OTM put for defined risk."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 50
|
|
26
|
+
param :spread_width, type: :number, default: 200
|
|
27
|
+
param :max_loss, type: :number, default: 5000
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_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_strikes(ctx)
|
|
51
|
+
spot = ctx[:spot_price].to_f
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
spread = ctx[:spread_width].to_f
|
|
54
|
+
|
|
55
|
+
atm_strike_price = nearest_strike(chain, spot)[:strike].to_f
|
|
56
|
+
|
|
57
|
+
short_put = find_strike(chain, atm_strike_price - spread)
|
|
58
|
+
long_put = find_strike(chain, atm_strike_price - (spread * 2))
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "Could not build bull put spread — insufficient strikes in chain" unless short_put && long_put
|
|
61
|
+
|
|
62
|
+
ctx[:legs] = [
|
|
63
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "PE", strike: short_put[:strike],
|
|
64
|
+
security_id: leg_security_id(short_put, "PE") },
|
|
65
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: long_put[:strike],
|
|
66
|
+
security_id: leg_security_id(long_put, "PE") }
|
|
67
|
+
]
|
|
68
|
+
ctx
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_intent(ctx)
|
|
72
|
+
ctx[:intent] = {
|
|
73
|
+
trade_type: "BULL_PUT_SPREAD",
|
|
74
|
+
symbol: ctx[:symbol],
|
|
75
|
+
expiry: ctx[:expiry],
|
|
76
|
+
quantity: ctx[:quantity],
|
|
77
|
+
spread_width: ctx[:spread_width],
|
|
78
|
+
max_loss: ctx[:max_loss],
|
|
79
|
+
legs: ctx[:legs],
|
|
80
|
+
note: "Bull put spread prepared. Await human confirmation before execution."
|
|
81
|
+
}
|
|
82
|
+
ctx
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
risk "trade_adjacent_read"
|
|
21
|
+
scope "orders:read"
|
|
22
|
+
description "Buy an at-the-money call option on an index (e.g. NIFTY)."
|
|
23
|
+
|
|
24
|
+
param :symbol, type: :string, required: true
|
|
25
|
+
param :expiry, type: :string, required: true
|
|
26
|
+
param :quantity, type: :integer, default: 50
|
|
27
|
+
param :stop_loss, type: :number, default: 100
|
|
28
|
+
param :target, type: :number, default: 200
|
|
29
|
+
|
|
30
|
+
step :find_instrument, priority: 1
|
|
31
|
+
step :get_spot_price, priority: 2
|
|
32
|
+
step :get_option_chain, priority: 3
|
|
33
|
+
step :select_atm_strike, priority: 4
|
|
34
|
+
step :prepare_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_atm_strike(ctx)
|
|
52
|
+
spot = ctx[:spot_price]
|
|
53
|
+
chain = ctx[:chain]
|
|
54
|
+
|
|
55
|
+
atm = nearest_strike(chain, spot)
|
|
56
|
+
raise ArgumentError, "Could not find ATM strike" unless atm
|
|
57
|
+
|
|
58
|
+
ctx[:selected_option] = atm
|
|
59
|
+
ctx[:security_id] = leg_security_id(atm, "CE")
|
|
60
|
+
ctx[:strike] = atm[:strike]
|
|
61
|
+
ctx[:premium] = leg_premium(atm, "CE")
|
|
62
|
+
ctx
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def prepare_intent(ctx)
|
|
66
|
+
ctx[:intent] = {
|
|
67
|
+
trade_type: "OPTIONS_BUY",
|
|
68
|
+
instrument: "#{ctx[:symbol]} #{ctx[:strike]} CE",
|
|
69
|
+
security_id: ctx[:security_id],
|
|
70
|
+
strike: ctx[:strike],
|
|
71
|
+
expiry: ctx[:expiry],
|
|
72
|
+
option_type: "CE",
|
|
73
|
+
quantity: ctx[:quantity],
|
|
74
|
+
premium: ctx[:premium],
|
|
75
|
+
stop_loss: ctx[:stop_loss],
|
|
76
|
+
target: ctx[:target],
|
|
77
|
+
note: "Prepared ATM call buy. Await human confirmation."
|
|
78
|
+
}
|
|
79
|
+
ctx
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Skills
|
|
5
|
+
module Builtin
|
|
6
|
+
# Skill to build a covered call strategy (buy 100 shares, sell 1 OTM call).
|
|
7
|
+
#
|
|
8
|
+
# Steps: find equity instrument → spot price → option chain →
|
|
9
|
+
# select OTM call strike → build intent.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# result = DhanHQ::Skills::Registry.call("covered_call",
|
|
13
|
+
# symbol: "RELIANCE",
|
|
14
|
+
# expiry: "2026-01-30",
|
|
15
|
+
# quantity: 100
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
class CoveredCall < Base
|
|
19
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build a covered call: buy the underlying equity, sell an OTM call against it."
|
|
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 :stop_loss, type: :number, default: nil
|
|
28
|
+
param :target, type: :number, default: nil
|
|
29
|
+
|
|
30
|
+
step :find_instrument, priority: 1
|
|
31
|
+
step :get_spot_price, priority: 2
|
|
32
|
+
step :get_option_chain, priority: 3
|
|
33
|
+
step :select_otm_call, priority: 4
|
|
34
|
+
step :build_intent, priority: 5
|
|
35
|
+
|
|
36
|
+
def find_instrument(ctx)
|
|
37
|
+
ctx[:instrument] = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::NSE_EQ, 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_otm_call(ctx)
|
|
52
|
+
spot = ctx[:spot_price].to_f
|
|
53
|
+
chain = ctx[:chain]
|
|
54
|
+
offset_pct = ctx[:strike_offset] / 100.0
|
|
55
|
+
|
|
56
|
+
target_strike = spot * (1 + offset_pct)
|
|
57
|
+
otm_call = nearest_strike(chain, target_strike)
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "Could not find suitable OTM call strike near #{target_strike}" unless otm_call
|
|
60
|
+
|
|
61
|
+
ctx[:call_strike] = otm_call[:strike]
|
|
62
|
+
ctx[:call_security_id] = leg_security_id(otm_call, "CE")
|
|
63
|
+
ctx[:call_premium] = leg_premium(otm_call, "CE")
|
|
64
|
+
ctx[:equity_security_id] = ctx[:instrument].security_id
|
|
65
|
+
ctx
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_intent(ctx)
|
|
69
|
+
ctx[:intent] = {
|
|
70
|
+
trade_type: "COVERED_CALL",
|
|
71
|
+
symbol: ctx[:symbol],
|
|
72
|
+
quantity: ctx[:quantity],
|
|
73
|
+
legs: [
|
|
74
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, instrument_type: DhanHQ::Constants::InstrumentType::EQUITY, security_id: ctx[:equity_security_id],
|
|
75
|
+
quantity: ctx[:quantity] },
|
|
76
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "CE", strike: ctx[:call_strike], security_id: ctx[:call_security_id], quantity: ctx[:quantity], premium: ctx[:call_premium] }
|
|
77
|
+
],
|
|
78
|
+
note: "Covered call prepared: Buy #{ctx[:quantity]} #{ctx[:symbol]}, Sell #{ctx[:quantity]} #{ctx[:call_strike]} CE. Await human confirmation."
|
|
79
|
+
}
|
|
80
|
+
ctx
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
risk "trade_adjacent_read"
|
|
20
|
+
scope "orders:read"
|
|
21
|
+
description "Build an iron condor: sell OTM call + sell OTM put, buy further OTM call + put for protection."
|
|
22
|
+
|
|
23
|
+
param :symbol, type: :string, required: true
|
|
24
|
+
param :expiry, type: :string, required: true
|
|
25
|
+
param :quantity, type: :integer, default: 50
|
|
26
|
+
param :wing_width, type: :number, default: 200
|
|
27
|
+
param :max_loss, type: :number, default: 5000
|
|
28
|
+
|
|
29
|
+
step :find_instrument, priority: 1
|
|
30
|
+
step :get_spot_price, priority: 2
|
|
31
|
+
step :get_option_chain, priority: 3
|
|
32
|
+
step :select_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_strikes(ctx)
|
|
51
|
+
spot = ctx[:spot_price]
|
|
52
|
+
chain = ctx[:chain]
|
|
53
|
+
wing = ctx[:wing_width].to_f
|
|
54
|
+
|
|
55
|
+
atm_strike = nearest_strike(chain, spot)[:strike].to_f
|
|
56
|
+
|
|
57
|
+
short_ce = find_strike(chain, atm_strike + wing)
|
|
58
|
+
long_ce = find_strike(chain, atm_strike + (wing * 2))
|
|
59
|
+
short_pe = find_strike(chain, atm_strike - wing)
|
|
60
|
+
long_pe = find_strike(chain, atm_strike - (wing * 2))
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "Could not build iron condor — insufficient strikes in chain" unless short_ce && long_ce && short_pe && long_pe
|
|
63
|
+
|
|
64
|
+
ctx[:legs] = [
|
|
65
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "CE", strike: short_ce[:strike], security_id: leg_security_id(short_ce, "CE") },
|
|
66
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "CE", strike: long_ce[:strike], security_id: leg_security_id(long_ce, "CE") },
|
|
67
|
+
{ action: DhanHQ::Constants::TransactionType::SELL, option_type: "PE", strike: short_pe[:strike], security_id: leg_security_id(short_pe, "PE") },
|
|
68
|
+
{ action: DhanHQ::Constants::TransactionType::BUY, option_type: "PE", strike: long_pe[:strike], security_id: leg_security_id(long_pe, "PE") }
|
|
69
|
+
]
|
|
70
|
+
ctx
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def build_intent(ctx)
|
|
74
|
+
ctx[:intent] = {
|
|
75
|
+
trade_type: "IRON_CONDOR",
|
|
76
|
+
symbol: ctx[:symbol],
|
|
77
|
+
expiry: ctx[:expiry],
|
|
78
|
+
quantity: ctx[:quantity],
|
|
79
|
+
wing_width: ctx[:wing_width],
|
|
80
|
+
max_loss: ctx[:max_loss],
|
|
81
|
+
legs: ctx[:legs],
|
|
82
|
+
note: "Iron condor prepared. Await human confirmation before execution."
|
|
83
|
+
}
|
|
84
|
+
ctx
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Style/RescueModifier
|
|
4
|
+
# rubocop:disable Naming/VariableNumber
|
|
5
|
+
# rubocop:disable Style/NumericPredicate
|
|
6
|
+
# rubocop:disable Lint/AmbiguousOperatorPrecedence
|
|
7
|
+
|
|
8
|
+
require "date"
|
|
9
|
+
|
|
10
|
+
module DhanHQ
|
|
11
|
+
module Skills
|
|
12
|
+
module Builtin
|
|
13
|
+
# Programmatic Agent Skill to process and summarize market data.
|
|
14
|
+
# Exposes computed technical indicators and options statistics rather than raw data.
|
|
15
|
+
class MarketDataSummarizer < Base
|
|
16
|
+
risk "read_only"
|
|
17
|
+
scope "market:read"
|
|
18
|
+
description "Summarize technicals and/or option chain (PCR, OI walls, ATM strikes) for a symbol."
|
|
19
|
+
|
|
20
|
+
param :underlying_symbol, type: :string, required: true, description: "Underlying ticker symbol (e.g. NIFTY, RELIANCE)"
|
|
21
|
+
param :mode, type: :string, default: "both", description: "Analysis mode: both, technicals, or option_chain"
|
|
22
|
+
param :interval, type: :string, default: DhanHQ::Constants::Validity::DAY, description: "Timeframe for technical indicators"
|
|
23
|
+
param :range_days, type: :integer, default: 30, description: "Lookback period in days for indicator calculation"
|
|
24
|
+
param :expiry, type: :string, default: "nearest", description: "Specific option expiry date (YYYY-MM-DD) or 'nearest'"
|
|
25
|
+
param :strike_range, type: :integer, default: 5, description: "Number of strikes to include above and below ATM"
|
|
26
|
+
|
|
27
|
+
step :resolve_instrument, priority: 1
|
|
28
|
+
step :fetch_technicals, priority: 2
|
|
29
|
+
step :fetch_option_chain_summary, priority: 3
|
|
30
|
+
step :prepare_final_summary, priority: 4
|
|
31
|
+
|
|
32
|
+
def resolve_instrument(ctx)
|
|
33
|
+
symbol = ctx[:underlying_symbol].to_s.upcase.strip
|
|
34
|
+
inst = DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::IDX_I, symbol) rescue nil
|
|
35
|
+
inst ||= DhanHQ::Models::Instrument.find(DhanHQ::Constants::ExchangeSegment::NSE_EQ, symbol) rescue nil
|
|
36
|
+
inst ||= DhanHQ::Models::Instrument.find_anywhere(symbol) rescue nil
|
|
37
|
+
|
|
38
|
+
raise "Underlying symbol not found: #{symbol}" unless inst
|
|
39
|
+
|
|
40
|
+
ctx[:instrument] = inst
|
|
41
|
+
ctx
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fetch_technicals(ctx)
|
|
45
|
+
return ctx unless %w[both technicals].include?(ctx[:mode])
|
|
46
|
+
|
|
47
|
+
inst = ctx[:instrument]
|
|
48
|
+
to_date = Date.today.strftime("%Y-%m-%d")
|
|
49
|
+
from_date = (Date.today - [ctx[:range_days].to_i * 2, 100].max).strftime("%Y-%m-%d")
|
|
50
|
+
|
|
51
|
+
candles = DhanHQ::Models::HistoricalData.daily(
|
|
52
|
+
security_id: inst.security_id,
|
|
53
|
+
exchange_segment: inst.exchange_segment,
|
|
54
|
+
instrument: inst.instrument,
|
|
55
|
+
from_date: from_date,
|
|
56
|
+
to_date: to_date
|
|
57
|
+
) rescue []
|
|
58
|
+
|
|
59
|
+
if candles.any?
|
|
60
|
+
closes = candles.map { |c| c[:close].to_f }
|
|
61
|
+
latest_close = closes.last
|
|
62
|
+
|
|
63
|
+
sma_20 = closes.size >= 20 ? (closes.last(20).sum / 20.0).round(2) : nil
|
|
64
|
+
sma_50 = closes.size >= 50 ? (closes.last(50).sum / 50.0).round(2) : nil
|
|
65
|
+
ret_5d = closes.size >= 6 ? (((closes.last / closes[-6]) - 1.0) * 100).round(2) : nil
|
|
66
|
+
|
|
67
|
+
# RSI (14) Calculation
|
|
68
|
+
rsi_14 = calculate_rsi(closes)
|
|
69
|
+
|
|
70
|
+
ctx[:technical_summary] = {
|
|
71
|
+
ltp: latest_close,
|
|
72
|
+
sma_20: sma_20,
|
|
73
|
+
sma_50: sma_50,
|
|
74
|
+
return_5d_pct: ret_5d,
|
|
75
|
+
rsi_14: rsi_14,
|
|
76
|
+
data_points_analyzed: closes.size
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
# Fallback to LTP quote if daily candles fail
|
|
80
|
+
quote = inst.ltp rescue {}
|
|
81
|
+
ctx[:technical_summary] = {
|
|
82
|
+
ltp: quote[:ltp] || quote["ltp"] || 0.0,
|
|
83
|
+
note: "Failed to load historical candles; loaded quote snapshot instead."
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
ctx
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fetch_option_chain_summary(ctx)
|
|
90
|
+
return ctx unless %w[both option_chain].include?(ctx[:mode])
|
|
91
|
+
|
|
92
|
+
inst = ctx[:instrument]
|
|
93
|
+
underlying_seg = inst.exchange_segment == DhanHQ::Constants::ExchangeSegment::IDX_I ? DhanHQ::Constants::ExchangeSegment::IDX_I : DhanHQ::Constants::ExchangeSegment::NSE_EQ
|
|
94
|
+
|
|
95
|
+
target_expiry = ctx[:expiry]
|
|
96
|
+
if target_expiry.to_s.empty? || target_expiry == "nearest"
|
|
97
|
+
expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
|
|
98
|
+
underlying_scrip: inst.security_id.to_i,
|
|
99
|
+
underlying_seg: underlying_seg
|
|
100
|
+
)
|
|
101
|
+
target_expiry = expiries.first
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
return ctx if target_expiry.nil?
|
|
105
|
+
|
|
106
|
+
chain = DhanHQ::Models::OptionChain.fetch(
|
|
107
|
+
underlying_scrip: inst.security_id.to_i,
|
|
108
|
+
underlying_seg: underlying_seg,
|
|
109
|
+
expiry: target_expiry
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if chain
|
|
113
|
+
spot = chain[:last_price] || ctx[:technical_summary]&.[](:ltp)
|
|
114
|
+
strikes = chain[:strikes] || []
|
|
115
|
+
|
|
116
|
+
# Locate ATM
|
|
117
|
+
closest_strike_data = strikes.min_by { |s| (s[:strike].to_f - spot).abs }
|
|
118
|
+
closest_idx = strikes.index(closest_strike_data)
|
|
119
|
+
|
|
120
|
+
# Filter ATM +/- strike_range
|
|
121
|
+
range = ctx[:strike_range].to_i
|
|
122
|
+
start_idx = [0, closest_idx - range].max
|
|
123
|
+
end_idx = [strikes.size - 1, closest_idx + range].min
|
|
124
|
+
|
|
125
|
+
filtered_strikes = strikes[start_idx..end_idx].map do |s|
|
|
126
|
+
{
|
|
127
|
+
strike: s[:strike],
|
|
128
|
+
ce: s[:call] ? { security_id: s[:call][:security_id], ltp: s[:call][:last_price], oi: s[:call][:oi] } : nil,
|
|
129
|
+
pe: s[:put] ? { security_id: s[:put][:security_id], ltp: s[:put][:last_price], oi: s[:put][:oi] } : nil
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# PCR & OI Walls
|
|
134
|
+
total_ce_oi = strikes.sum { |s| s[:call]&.[](:oi).to_f }
|
|
135
|
+
total_pe_oi = strikes.sum { |s| s[:put]&.[](:oi).to_f }
|
|
136
|
+
pcr = total_ce_oi > 0 ? (total_pe_oi / total_ce_oi).round(3) : 0.0
|
|
137
|
+
|
|
138
|
+
ce_walls = strikes.reject { |s| s[:call].nil? }.sort_by { |s| -(s[:call][:oi] || 0) }.first(3).map { |s| { strike: s[:strike], oi: s[:call][:oi] } }
|
|
139
|
+
pe_walls = strikes.reject { |s| s[:put].nil? }.sort_by { |s| -(s[:put][:oi] || 0) }.first(3).map { |s| { strike: s[:strike], oi: s[:put][:oi] } }
|
|
140
|
+
|
|
141
|
+
ctx[:option_chain_summary] = {
|
|
142
|
+
expiry: target_expiry,
|
|
143
|
+
spot: spot,
|
|
144
|
+
pcr: pcr,
|
|
145
|
+
resistance_walls: ce_walls,
|
|
146
|
+
support_walls: pe_walls,
|
|
147
|
+
strikes: filtered_strikes
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
ctx
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def prepare_final_summary(ctx)
|
|
154
|
+
ctx[:summary] = {
|
|
155
|
+
symbol: ctx[:underlying_symbol],
|
|
156
|
+
timestamp: Time.now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
157
|
+
technicals: ctx[:technical_summary],
|
|
158
|
+
options: ctx[:option_chain_summary]
|
|
159
|
+
}
|
|
160
|
+
ctx
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def calculate_rsi(closes)
|
|
166
|
+
return nil if closes.size < 15
|
|
167
|
+
|
|
168
|
+
gains = []
|
|
169
|
+
losses = []
|
|
170
|
+
closes.each_cons(2) do |prev, curr|
|
|
171
|
+
diff = curr - prev
|
|
172
|
+
gains << (diff > 0 ? diff : 0.0)
|
|
173
|
+
losses << (diff < 0 ? -diff : 0.0)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
avg_gain = gains.first(14).sum / 14.0
|
|
177
|
+
avg_loss = losses.first(14).sum / 14.0
|
|
178
|
+
|
|
179
|
+
gains[14..].zip(losses[14..]).each do |g, l|
|
|
180
|
+
avg_gain = (avg_gain * 13 + g) / 14.0
|
|
181
|
+
avg_loss = (avg_loss * 13 + l) / 14.0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
rs = avg_loss > 0 ? (avg_gain / avg_loss) : 100.0
|
|
185
|
+
(100.0 - (100.0 / (1.0 + rs))).round(2)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# rubocop:enable Style/RescueModifier
|
|
193
|
+
# rubocop:enable Naming/VariableNumber
|
|
194
|
+
# rubocop:enable Style/NumericPredicate
|
|
195
|
+
# rubocop:enable Lint/AmbiguousOperatorPrecedence
|