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
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
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Event-driven architecture for the trading system.
|
|
5
|
+
#
|
|
6
|
+
# Provides pub/sub event bus, typed events, and async helpers.
|
|
7
|
+
#
|
|
8
|
+
# @example Subscribe to events
|
|
9
|
+
# DhanHQ::Events.on(:order_filled) do |event|
|
|
10
|
+
# puts "Order filled: #{event.order_id}"
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# @example Emit events
|
|
14
|
+
# DhanHQ::Events.emit(:order_filled, order_id: "123", filled_quantity: 10)
|
|
15
|
+
#
|
|
16
|
+
module Events
|
|
17
|
+
# Simple pub/sub event bus.
|
|
18
|
+
#
|
|
19
|
+
# Supports subscribing to specific event types or all events.
|
|
20
|
+
class Bus
|
|
21
|
+
def initialize
|
|
22
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
23
|
+
@global_subscribers = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Subscribe to an event type.
|
|
27
|
+
#
|
|
28
|
+
# @param event_type [Symbol, Class] Event type to subscribe to
|
|
29
|
+
# @param block [Proc] Handler block
|
|
30
|
+
# @return [Integer] Subscription ID for unsubscribing
|
|
31
|
+
def on(event_type, &block)
|
|
32
|
+
@subscribers[event_type] << block
|
|
33
|
+
@subscribers[event_type].length - 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Subscribe to all events.
|
|
37
|
+
#
|
|
38
|
+
# @param block [Proc] Handler block
|
|
39
|
+
# @return [Integer] Subscription ID
|
|
40
|
+
def subscribe_all(&block)
|
|
41
|
+
@global_subscribers << block
|
|
42
|
+
@global_subscribers.length - 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Unsubscribe from an event type.
|
|
46
|
+
#
|
|
47
|
+
# @param event_type [Symbol, Class] Event type
|
|
48
|
+
# @param subscription_id [Integer] Subscription ID from on()
|
|
49
|
+
# @return [Boolean] True if unsubscribed
|
|
50
|
+
# rubocop:disable Naming/PredicateMethod
|
|
51
|
+
def off(event_type, subscription_id)
|
|
52
|
+
return false unless @subscribers[event_type]
|
|
53
|
+
|
|
54
|
+
@subscribers[event_type].delete_at(subscription_id)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Unsubscribe from all events.
|
|
59
|
+
#
|
|
60
|
+
# @param subscription_id [Integer] Subscription ID
|
|
61
|
+
# @return [Boolean] True if unsubscribed
|
|
62
|
+
def unsubscribe_all(subscription_id)
|
|
63
|
+
@global_subscribers.delete_at(subscription_id)
|
|
64
|
+
true
|
|
65
|
+
end
|
|
66
|
+
# rubocop:enable Naming/PredicateMethod
|
|
67
|
+
|
|
68
|
+
# Emit an event to all subscribers.
|
|
69
|
+
#
|
|
70
|
+
# @param event_type [Symbol, Class] Event type
|
|
71
|
+
# @param data [Hash] Event data
|
|
72
|
+
# @return [void]
|
|
73
|
+
def emit(event_type, data = {})
|
|
74
|
+
event = build_event(event_type, data)
|
|
75
|
+
|
|
76
|
+
# Notify type-specific subscribers
|
|
77
|
+
@subscribers[event_type]&.each do |handler|
|
|
78
|
+
handler.call(event)
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
DhanHQ.logger&.error("[Events] Error in handler for #{event_type}: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Notify global subscribers
|
|
84
|
+
@global_subscribers.each do |handler|
|
|
85
|
+
handler.call(event_type, event)
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
DhanHQ.logger&.error("[Events] Error in global handler: #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get count of subscribers for an event type.
|
|
92
|
+
#
|
|
93
|
+
# @param event_type [Symbol] Event type
|
|
94
|
+
# @return [Integer]
|
|
95
|
+
def subscriber_count(event_type)
|
|
96
|
+
@subscribers[event_type]&.length || 0
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Clear all subscribers.
|
|
100
|
+
def clear
|
|
101
|
+
@subscribers.clear
|
|
102
|
+
@global_subscribers.clear
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def build_event(event_type, data)
|
|
108
|
+
case event_type
|
|
109
|
+
when :order_placed then OrderPlaced.new(data)
|
|
110
|
+
when :order_filled then OrderFilled.new(data)
|
|
111
|
+
when :order_cancelled then OrderCancelled.new(data)
|
|
112
|
+
when :order_rejected then OrderRejected.new(data)
|
|
113
|
+
when :sl_hit then SLHit.new(data)
|
|
114
|
+
when :tp_hit then TPHit.new(data)
|
|
115
|
+
when :tick_updated then TickUpdated.new(data)
|
|
116
|
+
when :position_opened then PositionOpened.new(data)
|
|
117
|
+
when :position_closed then PositionClosed.new(data)
|
|
118
|
+
when :strategy_signal then StrategySignal.new(data)
|
|
119
|
+
else
|
|
120
|
+
Base.new(data.merge(event_type: event_type))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Default bus instance
|
|
126
|
+
@bus = Bus.new
|
|
127
|
+
|
|
128
|
+
class << self
|
|
129
|
+
# Get the default event bus.
|
|
130
|
+
attr_reader :bus
|
|
131
|
+
|
|
132
|
+
# Subscribe to an event type (delegates to bus).
|
|
133
|
+
def on(event_type, &)
|
|
134
|
+
bus.on(event_type, &)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Subscribe to all events (delegates to bus).
|
|
138
|
+
def subscribe_all(&)
|
|
139
|
+
bus.subscribe_all(&)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Emit an event (delegates to bus).
|
|
143
|
+
def emit(event_type, data = {})
|
|
144
|
+
bus.emit(event_type, data)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Unsubscribe from an event type (delegates to bus).
|
|
148
|
+
def off(event_type, subscription_id)
|
|
149
|
+
bus.off(event_type, subscription_id)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Clear all subscribers (delegates to bus).
|
|
153
|
+
def clear
|
|
154
|
+
bus.clear
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "events/base"
|
|
4
|
+
require_relative "events/bus"
|
|
5
|
+
|
|
6
|
+
module DhanHQ
|
|
7
|
+
# Event-driven architecture for the trading system.
|
|
8
|
+
#
|
|
9
|
+
# Provides typed events, pub/sub event bus, and async helpers
|
|
10
|
+
# for building reactive trading applications.
|
|
11
|
+
#
|
|
12
|
+
# @example Subscribe to order events
|
|
13
|
+
# DhanHQ::Events.on(:order_filled) do |event|
|
|
14
|
+
# puts "Order filled: #{event.order_id}"
|
|
15
|
+
# puts "Filled quantity: #{event.filled_quantity}"
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Emit events
|
|
19
|
+
# DhanHQ::Events.emit(:order_filled, order_id: "123", filled_quantity: 10)
|
|
20
|
+
#
|
|
21
|
+
# @example Subscribe to all events
|
|
22
|
+
# DhanHQ::Events.subscribe_all do |event_type, event|
|
|
23
|
+
# puts "Event: #{event_type} - #{event}"
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Available event types:
|
|
27
|
+
# - :order_placed - Order placed successfully
|
|
28
|
+
# - :order_filled - Order fully or partially filled
|
|
29
|
+
# - :order_cancelled - Order cancelled
|
|
30
|
+
# - :order_rejected - Order rejected by exchange
|
|
31
|
+
# - :sl_hit - Stop loss triggered
|
|
32
|
+
# - :tp_hit - Take profit triggered
|
|
33
|
+
# - :tick_updated - New market tick received
|
|
34
|
+
# - :position_opened - New position opened
|
|
35
|
+
# - :position_closed - Position closed
|
|
36
|
+
# - :strategy_signal - Strategy generated a signal
|
|
37
|
+
#
|
|
38
|
+
module Events
|
|
39
|
+
end
|
|
40
|
+
end
|