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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +7 -0
  3. data/lib/DhanHQ/agent/order_preview.rb +50 -0
  4. data/lib/DhanHQ/agent/policy.rb +51 -0
  5. data/lib/DhanHQ/agent/tool_registry.rb +250 -0
  6. data/lib/DhanHQ/agent.rb +12 -0
  7. data/lib/DhanHQ/ai/context_builder.rb +145 -0
  8. data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
  9. data/lib/DhanHQ/ai.rb +27 -0
  10. data/lib/DhanHQ/auth.rb +0 -1
  11. data/lib/DhanHQ/client.rb +1 -3
  12. data/lib/DhanHQ/constants.rb +2 -0
  13. data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
  14. data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
  15. data/lib/DhanHQ/core/auth_api.rb +0 -1
  16. data/lib/DhanHQ/errors.rb +4 -0
  17. data/lib/DhanHQ/events/base.rb +203 -0
  18. data/lib/DhanHQ/events/bus.rb +158 -0
  19. data/lib/DhanHQ/events.rb +40 -0
  20. data/lib/DhanHQ/indicators.rb +283 -0
  21. data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
  22. data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
  23. data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
  24. data/lib/DhanHQ/market_data.rb +25 -0
  25. data/lib/DhanHQ/mcp/server.rb +72 -0
  26. data/lib/DhanHQ/mcp.rb +10 -0
  27. data/lib/DhanHQ/models/funds.rb +12 -0
  28. data/lib/DhanHQ/models/holding.rb +42 -0
  29. data/lib/DhanHQ/models/iceberg_order.rb +139 -0
  30. data/lib/DhanHQ/models/instrument.rb +36 -0
  31. data/lib/DhanHQ/models/order.rb +95 -0
  32. data/lib/DhanHQ/models/position.rb +66 -0
  33. data/lib/DhanHQ/models/search_result.rb +12 -0
  34. data/lib/DhanHQ/models/trade.rb +13 -0
  35. data/lib/DhanHQ/models/twap_order.rb +136 -0
  36. data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
  37. data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
  38. data/lib/DhanHQ/option_analytics.rb +36 -0
  39. data/lib/DhanHQ/resources/iceberg_orders.rb +61 -0
  40. data/lib/DhanHQ/resources/twap_orders.rb +61 -0
  41. data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
  42. data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
  43. data/lib/DhanHQ/risk/checks/options.rb +46 -0
  44. data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
  45. data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
  46. data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
  47. data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
  48. data/lib/DhanHQ/risk/pipeline.rb +65 -0
  49. data/lib/DhanHQ/risk.rb +250 -0
  50. data/lib/DhanHQ/skills/base.rb +132 -0
  51. data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +87 -0
  52. data/lib/DhanHQ/skills/builtin/iron_condor.rb +93 -0
  53. data/lib/DhanHQ/skills/builtin/square_off_all.rb +45 -0
  54. data/lib/DhanHQ/skills/builtin/square_off_position.rb +48 -0
  55. data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
  56. data/lib/DhanHQ/skills/registry.rb +101 -0
  57. data/lib/DhanHQ/skills/workflow.rb +66 -0
  58. data/lib/DhanHQ/skills.rb +29 -0
  59. data/lib/DhanHQ/strategy/base.rb +189 -0
  60. data/lib/DhanHQ/strategy.rb +40 -0
  61. data/lib/DhanHQ/version.rb +1 -1
  62. data/lib/DhanHQ/ws/decoder.rb +57 -19
  63. data/lib/DhanHQ.rb +3 -0
  64. data/lib/dhan_hq/agent.rb +3 -0
  65. data/lib/dhan_hq/mcp.rb +3 -0
  66. data/lib/dhan_hq.rb +27 -2
  67. data/lib/ta/technical_analysis.rb +3 -1
  68. data/skills/dhanhq-ruby/SKILL.md +74 -0
  69. data/skills/dhanhq-ruby/references/market_data.md +3 -0
  70. data/skills/dhanhq-ruby/references/orders.md +7 -0
  71. metadata +59 -20
data/lib/DhanHQ/auth.rb CHANGED
@@ -91,7 +91,6 @@ module DhanHQ
91
91
  def self.build_connection(base_url)
92
92
  Faraday.new(url: base_url) do |c|
93
93
  c.request :url_encoded
94
- c.response :json, content_type: /\bjson$/
95
94
  c.adapter Faraday.default_adapter
96
95
  end
97
96
  end
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 :json, parser_options: { symbolize_names: true }
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
@@ -475,6 +475,8 @@ module DhanHQ
475
475
  PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES = %w[
476
476
  /alerts/orders
477
477
  /v2/orders
478
+ /v2/orders/iceberg
479
+ /v2/orders/twap
478
480
  /v2/forever
479
481
  /v2/super/orders
480
482
  /v2/positions
@@ -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
@@ -13,7 +13,6 @@ module DhanHQ
13
13
  def connection
14
14
  @connection ||= Faraday.new(url: BASE_URL) do |faraday|
15
15
  faraday.request :url_encoded
16
- faraday.response :json, content_type: /\bjson$/
17
16
  faraday.adapter Faraday.default_adapter
18
17
  end
19
18
  end
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