DhanHQ 2.8.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/CHANGELOG.md +19 -0
- data/README.md +16 -5
- data/docs/RAILS_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/TECHNICAL_ANALYSIS.md +1 -1
- data/exe/DhanHQ +1 -1
- 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/analysis.rb +9 -0
- data/lib/dhan_hq/mcp.rb +3 -0
- data/lib/dhan_hq/ta.rb +5 -0
- data/lib/dhan_hq.rb +27 -4
- 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 +61 -20
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module AI
|
|
5
|
+
# Helper methods for generating prompts for AI trading assistants.
|
|
6
|
+
#
|
|
7
|
+
# Provides methods to create system prompts, user prompts,
|
|
8
|
+
# and context summaries for AI models.
|
|
9
|
+
module PromptHelpers
|
|
10
|
+
# Generate a system prompt for an AI trading assistant.
|
|
11
|
+
#
|
|
12
|
+
# @param capabilities [Array<String>] List of capabilities
|
|
13
|
+
# @return [String] System prompt
|
|
14
|
+
def self.system_prompt(capabilities: [])
|
|
15
|
+
<<~PROMPT.strip
|
|
16
|
+
You are an AI trading assistant for Indian stock markets (NSE, BSE, MCX).
|
|
17
|
+
|
|
18
|
+
Your capabilities:
|
|
19
|
+
- Fetch market data (LTP, OHLC, quotes)
|
|
20
|
+
- Place, modify, and cancel orders
|
|
21
|
+
- View portfolio holdings, positions, and orders
|
|
22
|
+
- Calculate option Greeks and implied volatility
|
|
23
|
+
- Analyze option chains and Max Pain
|
|
24
|
+
- Apply risk management rules
|
|
25
|
+
|
|
26
|
+
#{"Additional capabilities:\n#{capabilities.map { |c| "- #{c}" }.join("\n")}" unless capabilities.empty?}
|
|
27
|
+
|
|
28
|
+
Rules:
|
|
29
|
+
- Always confirm before placing live orders
|
|
30
|
+
- Use correlation_id for all agent-originated orders
|
|
31
|
+
- Never expose access tokens or secrets
|
|
32
|
+
- Prefer read-only operations unless explicitly asked to trade
|
|
33
|
+
- Validate instruments before trading using search
|
|
34
|
+
PROMPT
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Generate a portfolio summary prompt.
|
|
38
|
+
#
|
|
39
|
+
# @param holdings [Array<DhanHQ::Models::Holding>] Holdings data
|
|
40
|
+
# @param positions [Array<DhanHQ::Models::Position>] Positions data
|
|
41
|
+
# @param funds [DhanHQ::Models::Funds] Funds data
|
|
42
|
+
# @return [String] Portfolio summary
|
|
43
|
+
def self.portfolio_summary(holdings:, positions:, funds:)
|
|
44
|
+
lines = ["=== Portfolio Summary ==="]
|
|
45
|
+
lines << "Funds: #{funds.to_prompt}" if funds
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << "Holdings (#{holdings.size}):"
|
|
48
|
+
holdings.each { |h| lines << " #{h.to_prompt}" }
|
|
49
|
+
lines << ""
|
|
50
|
+
lines << "Open Positions (#{positions.count(&:open?)}):"
|
|
51
|
+
positions.select(&:open?).each { |p| lines << " #{p.to_prompt}" }
|
|
52
|
+
lines.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate a market analysis prompt.
|
|
56
|
+
#
|
|
57
|
+
# @param snapshot [DhanHQ::MarketData::MarketSnapshot] Market snapshot
|
|
58
|
+
# @param series [DhanHQ::MarketData::OHLCSeries] OHLC series
|
|
59
|
+
# @return [String] Market analysis prompt
|
|
60
|
+
def self.market_analysis(snapshot:, series: nil)
|
|
61
|
+
lines = ["=== Market Analysis ==="]
|
|
62
|
+
lines << "Snapshot: #{snapshot.size} instruments"
|
|
63
|
+
lines << "Series: #{series&.size || 0} candles"
|
|
64
|
+
|
|
65
|
+
if series&.any?
|
|
66
|
+
lines << "Latest close: ₹#{series.last.close}"
|
|
67
|
+
lines << "Average close: ₹#{series.average_close&.round(2)}"
|
|
68
|
+
lines << "Price range: ₹#{series.price_range&.round(2)}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Generate an order confirmation prompt.
|
|
75
|
+
#
|
|
76
|
+
# @param order_params [Hash] Order parameters
|
|
77
|
+
# @return [String] Order confirmation prompt
|
|
78
|
+
def self.order_confirmation(order_params)
|
|
79
|
+
<<~PROMPT.strip
|
|
80
|
+
=== Order Confirmation Required ===
|
|
81
|
+
#{order_params[:transaction_type]} #{order_params[:quantity]}x #{order_params[:security_id]}
|
|
82
|
+
Exchange: #{order_params[:exchange_segment]}
|
|
83
|
+
Product: #{order_params[:product_type]}
|
|
84
|
+
Type: #{order_params[:order_type]}
|
|
85
|
+
#{order_params[:price] ? "Price: ₹#{order_params[:price]}" : "Market Price"}
|
|
86
|
+
#{"Trigger: ₹#{order_params[:trigger_price]}" if order_params[:trigger_price]}
|
|
87
|
+
|
|
88
|
+
Please confirm this order.
|
|
89
|
+
PROMPT
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Generate a risk report prompt.
|
|
93
|
+
#
|
|
94
|
+
# @param positions [Array<DhanHQ::Models::Position>] Current positions
|
|
95
|
+
# @param risk_params [Hash] Risk parameters
|
|
96
|
+
# @return [String] Risk report
|
|
97
|
+
def self.risk_report(positions:, risk_params: {})
|
|
98
|
+
lines = ["=== Risk Report ==="]
|
|
99
|
+
total_unrealized = positions.sum { |p| p.unrealized_profit.to_f }
|
|
100
|
+
total_realized = positions.sum { |p| p.realized_profit.to_f }
|
|
101
|
+
|
|
102
|
+
lines << "Total Unrealized P&L: ₹#{total_unrealized.round(2)}"
|
|
103
|
+
lines << "Total Realized P&L: ₹#{total_realized.round(2)}"
|
|
104
|
+
lines << "Open Positions: #{positions.count(&:open?)}"
|
|
105
|
+
|
|
106
|
+
lines << "Max Drawdown: #{risk_params[:max_drawdown]}%" if risk_params[:max_drawdown]
|
|
107
|
+
|
|
108
|
+
lines << "Daily Loss Limit: ₹#{risk_params[:daily_loss_limit]}" if risk_params[:daily_loss_limit]
|
|
109
|
+
|
|
110
|
+
lines.join("\n")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/DhanHQ/ai.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ai/context_builder"
|
|
4
|
+
require_relative "ai/prompt_helpers"
|
|
5
|
+
|
|
6
|
+
module DhanHQ
|
|
7
|
+
# AI integration layer for building AI-powered trading assistants.
|
|
8
|
+
#
|
|
9
|
+
# Provides workflow orchestration, prompt helpers, and context
|
|
10
|
+
# serialization for AI agents.
|
|
11
|
+
#
|
|
12
|
+
# @example Build context for AI
|
|
13
|
+
# context = DhanHQ::AI::ContextBuilder.build do |ctx|
|
|
14
|
+
# ctx.add_portfolio
|
|
15
|
+
# ctx.add_positions
|
|
16
|
+
# ctx.add_recent_orders(limit: 10)
|
|
17
|
+
# end
|
|
18
|
+
# puts context.to_prompt
|
|
19
|
+
#
|
|
20
|
+
# @example Generate system prompt
|
|
21
|
+
# prompt = DhanHQ::AI::PromptHelpers.system_prompt(
|
|
22
|
+
# capabilities: ["Option chain analysis", "Greeks calculation"]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
module AI
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/DhanHQ/auth.rb
CHANGED
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -212,9 +212,7 @@ module DhanHQ
|
|
|
212
212
|
write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
|
|
213
213
|
|
|
214
214
|
Faraday.new(url: url) do |conn|
|
|
215
|
-
conn.request :
|
|
216
|
-
conn.response :json, content_type: /\bjson$/
|
|
217
|
-
conn.response :logger if ENV["DHAN_DEBUG"] == "true"
|
|
215
|
+
conn.request :url_encoded
|
|
218
216
|
conn.options.timeout = read_timeout
|
|
219
217
|
conn.options.open_timeout = connect_timeout
|
|
220
218
|
conn.options.write_timeout = write_timeout
|
data/lib/DhanHQ/constants.rb
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_contract"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Contracts
|
|
7
|
+
# Validates request for POST /v2/orders/iceberg (create Iceberg order).
|
|
8
|
+
#
|
|
9
|
+
# Iceberg orders split a large order into multiple visible legs of a fixed
|
|
10
|
+
# disclosed quantity, reducing market impact.
|
|
11
|
+
class IcebergOrderCreateContract < BaseContract
|
|
12
|
+
params do
|
|
13
|
+
required(:dhan_client_id).filled(:string)
|
|
14
|
+
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
15
|
+
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
16
|
+
required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
|
|
17
|
+
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
18
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
19
|
+
required(:security_id).filled(:string)
|
|
20
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
21
|
+
required(:price).filled(:float, gt?: 0)
|
|
22
|
+
required(:iceberg_qty).filled(:integer, gt?: 0)
|
|
23
|
+
required(:disclosed_quantity).filled(:integer, gteq?: 0)
|
|
24
|
+
optional(:correlation_id).maybe(:string, max_size?: 30, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
25
|
+
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
26
|
+
optional(:after_market_order).maybe(:bool)
|
|
27
|
+
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|
|
28
|
+
optional(:drv_expiry_date).maybe(:string)
|
|
29
|
+
optional(:drv_option_type).maybe(:string, included_in?: %w[CALL PUT NA])
|
|
30
|
+
optional(:drv_strike_price).maybe(:float, gt?: 0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
rule(:iceberg_qty) do
|
|
34
|
+
key.failure("must not exceed total quantity") if value && values[:quantity] && value > values[:quantity]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validates request for PUT /v2/orders/iceberg/{order-id} (modify Iceberg order).
|
|
39
|
+
class IcebergOrderModifyContract < BaseContract
|
|
40
|
+
params do
|
|
41
|
+
required(:dhan_client_id).filled(:string)
|
|
42
|
+
required(:order_id).filled(:string)
|
|
43
|
+
|
|
44
|
+
optional(:transaction_type).maybe(:string, included_in?: TRANSACTION_TYPES)
|
|
45
|
+
optional(:exchange_segment).maybe(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
46
|
+
optional(:product_type).maybe(:string, included_in?: PRODUCT_TYPES)
|
|
47
|
+
optional(:order_type).maybe(:string, included_in?: ORDER_TYPES)
|
|
48
|
+
optional(:validity).maybe(:string, included_in?: VALIDITY_TYPES)
|
|
49
|
+
optional(:security_id).maybe(:string, max_size?: 20)
|
|
50
|
+
optional(:quantity).maybe(:integer, gt?: 0)
|
|
51
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
52
|
+
optional(:iceberg_qty).maybe(:integer, gt?: 0)
|
|
53
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
54
|
+
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
55
|
+
optional(:after_market_order).maybe(:bool)
|
|
56
|
+
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
rule do
|
|
60
|
+
modifiable_fields = %i[
|
|
61
|
+
quantity price iceberg_qty disclosed_quantity
|
|
62
|
+
trigger_price validity order_type product_type
|
|
63
|
+
]
|
|
64
|
+
changed = modifiable_fields.any? { |field| values.key?(field) && !values[field].nil? }
|
|
65
|
+
base.failure("at least one modifiable field must be provided") unless changed
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
rule(:order_type, :price) do
|
|
69
|
+
key(:price).failure("cannot modify price for MARKET orders") if values[:order_type] == DhanHQ::Constants::OrderType::MARKET && values[:price]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
rule(:order_type, :trigger_price) do
|
|
73
|
+
if %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type]) && (values[:trigger_price].nil? || values[:trigger_price].to_f <= 0)
|
|
74
|
+
key(:trigger_price).failure("must be present and greater than zero for STOP_LOSS orders")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
rule(:iceberg_qty) do
|
|
79
|
+
key.failure("must not exceed total quantity") if value && values[:quantity] && value > values[:quantity]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_contract"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Contracts
|
|
7
|
+
# Validates request for POST /v2/orders/twap (create TWAP order).
|
|
8
|
+
#
|
|
9
|
+
# TWAP orders slice the total quantity across the trading window at a fixed
|
|
10
|
+
# interval to minimize market impact and achieve time-weighted execution.
|
|
11
|
+
class TwapOrderCreateContract < BaseContract
|
|
12
|
+
params do
|
|
13
|
+
required(:dhan_client_id).filled(:string)
|
|
14
|
+
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
15
|
+
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
16
|
+
required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
|
|
17
|
+
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
18
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
19
|
+
required(:security_id).filled(:string)
|
|
20
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
21
|
+
required(:price).filled(:float, gt?: 0)
|
|
22
|
+
required(:slice_interval).filled(:integer, gt?: 0)
|
|
23
|
+
required(:start_time).filled(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
24
|
+
required(:end_time).filled(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
25
|
+
optional(:correlation_id).maybe(:string, max_size?: 30, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
26
|
+
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
27
|
+
optional(:after_market_order).maybe(:bool)
|
|
28
|
+
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|
|
29
|
+
optional(:drv_expiry_date).maybe(:string)
|
|
30
|
+
optional(:drv_option_type).maybe(:string, included_in?: %w[CALL PUT NA])
|
|
31
|
+
optional(:drv_strike_price).maybe(:float, gt?: 0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rule(:start_time, :end_time) do
|
|
35
|
+
next unless values[:start_time] && values[:end_time]
|
|
36
|
+
|
|
37
|
+
start_minutes = time_to_minutes(values[:start_time])
|
|
38
|
+
end_minutes = time_to_minutes(values[:end_time])
|
|
39
|
+
key.failure("must be after start_time") if end_minutes <= start_minutes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
rule(:slice_interval) do
|
|
43
|
+
next unless value&.positive?
|
|
44
|
+
|
|
45
|
+
start_minutes = time_to_minutes(values[:start_time])
|
|
46
|
+
end_minutes = time_to_minutes(values[:end_time])
|
|
47
|
+
window_minutes = end_minutes - start_minutes
|
|
48
|
+
key.failure("slice interval must fit within the execution window") if value > (window_minutes * 60)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def time_to_minutes(time_str)
|
|
52
|
+
parts = time_str.split(":").map(&:to_i)
|
|
53
|
+
(parts[0] * 60) + parts[1]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Validates request for PUT /v2/orders/twap/{order-id} (modify TWAP order).
|
|
58
|
+
class TwapOrderModifyContract < BaseContract
|
|
59
|
+
params do
|
|
60
|
+
required(:dhan_client_id).filled(:string)
|
|
61
|
+
required(:order_id).filled(:string)
|
|
62
|
+
|
|
63
|
+
optional(:transaction_type).maybe(:string, included_in?: TRANSACTION_TYPES)
|
|
64
|
+
optional(:exchange_segment).maybe(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
65
|
+
optional(:product_type).maybe(:string, included_in?: PRODUCT_TYPES)
|
|
66
|
+
optional(:order_type).maybe(:string, included_in?: ORDER_TYPES)
|
|
67
|
+
optional(:validity).maybe(:string, included_in?: VALIDITY_TYPES)
|
|
68
|
+
optional(:security_id).maybe(:string, max_size?: 20)
|
|
69
|
+
optional(:quantity).maybe(:integer, gt?: 0)
|
|
70
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
71
|
+
optional(:slice_interval).maybe(:integer, gt?: 0)
|
|
72
|
+
optional(:start_time).maybe(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
73
|
+
optional(:end_time).maybe(:string, format?: /\A([01]?\d|2[0-3]):([0-5]?\d):([0-5]?\d)\z/)
|
|
74
|
+
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
75
|
+
optional(:after_market_order).maybe(:bool)
|
|
76
|
+
optional(:amo_time).maybe(:string, included_in?: AMO_TIMINGS)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
rule do
|
|
80
|
+
modifiable_fields = %i[
|
|
81
|
+
quantity price slice_interval start_time end_time
|
|
82
|
+
trigger_price validity order_type product_type
|
|
83
|
+
]
|
|
84
|
+
changed = modifiable_fields.any? { |field| values.key?(field) && !values[field].nil? }
|
|
85
|
+
base.failure("at least one modifiable field must be provided") unless changed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
rule(:order_type, :price) do
|
|
89
|
+
key(:price).failure("cannot modify price for MARKET orders") if values[:order_type] == DhanHQ::Constants::OrderType::MARKET && values[:price]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
rule(:start_time, :end_time) do
|
|
93
|
+
next unless values[:start_time] && values[:end_time]
|
|
94
|
+
|
|
95
|
+
start_minutes = time_to_minutes(values[:start_time])
|
|
96
|
+
end_minutes = time_to_minutes(values[:end_time])
|
|
97
|
+
key.failure("must be after start_time") if end_minutes <= start_minutes
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def time_to_minutes(time_str)
|
|
101
|
+
parts = time_str.split(":").map(&:to_i)
|
|
102
|
+
(parts[0] * 60) + parts[1]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/DhanHQ/core/auth_api.rb
CHANGED
data/lib/DhanHQ/errors.rb
CHANGED
|
@@ -53,6 +53,10 @@ module DhanHQ
|
|
|
53
53
|
# Raised when the API signals an issue with the requested data payload.
|
|
54
54
|
class DataError < Error; end
|
|
55
55
|
|
|
56
|
+
# Risk management errors
|
|
57
|
+
# Raised when a pre-execution risk check fails (ASM/GSM, quantity, market hours, etc.)
|
|
58
|
+
class RiskViolation < Error; end
|
|
59
|
+
|
|
56
60
|
# Server and network-related errors
|
|
57
61
|
# DH-908, 800
|
|
58
62
|
class InternalServerError < Error; end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Event types for the trading system.
|
|
5
|
+
#
|
|
6
|
+
# Provides typed event classes for order lifecycle, market data,
|
|
7
|
+
# position updates, and strategy signals.
|
|
8
|
+
#
|
|
9
|
+
# @example Subscribe to order events
|
|
10
|
+
# DhanHQ::Events.on(:order_filled) do |event|
|
|
11
|
+
# puts "Order filled: #{event.order_id}"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
module Events
|
|
15
|
+
# Base event class with common attributes.
|
|
16
|
+
class Base
|
|
17
|
+
attr_reader :timestamp, :data
|
|
18
|
+
|
|
19
|
+
def initialize(data = {})
|
|
20
|
+
@timestamp = Time.now
|
|
21
|
+
@data = data
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
event_type: self.class.name.split("::").last.downcase.to_sym,
|
|
27
|
+
timestamp: timestamp,
|
|
28
|
+
data: data
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_prompt
|
|
33
|
+
"#{self.class.name.split("::").last}: #{data.inspect}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Order placed event.
|
|
38
|
+
class OrderPlaced < Base
|
|
39
|
+
def order_id
|
|
40
|
+
data[:order_id]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
"OrderPlaced(#{order_id})"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Order filled event.
|
|
49
|
+
class OrderFilled < Base
|
|
50
|
+
def order_id
|
|
51
|
+
data[:order_id]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def filled_quantity
|
|
55
|
+
data[:filled_quantity]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def filled_price
|
|
59
|
+
data[:filled_price]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
"OrderFilled(#{order_id}, #{filled_quantity}@#{filled_price})"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Order cancelled event.
|
|
68
|
+
class OrderCancelled < Base
|
|
69
|
+
def order_id
|
|
70
|
+
data[:order_id]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def reason
|
|
74
|
+
data[:reason]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_s
|
|
78
|
+
"OrderCancelled(#{order_id}, reason=#{reason})"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Order rejected event.
|
|
83
|
+
class OrderRejected < Base
|
|
84
|
+
def order_id
|
|
85
|
+
data[:order_id]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def error_code
|
|
89
|
+
data[:error_code]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def error_message
|
|
93
|
+
data[:error_message]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def to_s
|
|
97
|
+
"OrderRejected(#{order_id}, #{error_code}: #{error_message})"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Stop loss hit event.
|
|
102
|
+
class SLHit < Base
|
|
103
|
+
def order_id
|
|
104
|
+
data[:order_id]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def trigger_price
|
|
108
|
+
data[:trigger_price]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_s
|
|
112
|
+
"SLHit(#{order_id}, trigger=#{trigger_price})"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Take profit hit event.
|
|
117
|
+
class TPHit < Base
|
|
118
|
+
def order_id
|
|
119
|
+
data[:order_id]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def target_price
|
|
123
|
+
data[:target_price]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def to_s
|
|
127
|
+
"TPHit(#{order_id}, target=#{target_price})"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Market data tick event.
|
|
132
|
+
class TickUpdated < Base
|
|
133
|
+
def security_id
|
|
134
|
+
data[:security_id]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def ltp
|
|
138
|
+
data[:ltp]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def volume
|
|
142
|
+
data[:volume]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def to_s
|
|
146
|
+
"TickUpdated(#{security_id}, ltp=#{ltp})"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Position opened event.
|
|
151
|
+
class PositionOpened < Base
|
|
152
|
+
def security_id
|
|
153
|
+
data[:security_id]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def quantity
|
|
157
|
+
data[:quantity]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def side
|
|
161
|
+
data[:side]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_s
|
|
165
|
+
"PositionOpened(#{security_id}, #{side} #{quantity})"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Position closed event.
|
|
170
|
+
class PositionClosed < Base
|
|
171
|
+
def security_id
|
|
172
|
+
data[:security_id]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def profit_loss
|
|
176
|
+
data[:profit_loss]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def to_s
|
|
180
|
+
"PositionClosed(#{security_id}, pnl=#{profit_loss})"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Strategy signal event.
|
|
185
|
+
class StrategySignal < Base
|
|
186
|
+
def strategy_name
|
|
187
|
+
data[:strategy_name]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def signal_type
|
|
191
|
+
data[:signal_type]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def strength
|
|
195
|
+
data[:strength]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def to_s
|
|
199
|
+
"StrategySignal(#{strategy_name}, #{signal_type}, strength=#{strength})"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|