DhanHQ 2.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.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/GUIDE.md +555 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +463 -0
  9. data/README1.md +521 -0
  10. data/Rakefile +12 -0
  11. data/TAGS +10 -0
  12. data/TODO-1.md +14 -0
  13. data/TODO.md +127 -0
  14. data/app/services/live/order_update_guard_support.rb +75 -0
  15. data/app/services/live/order_update_hub.rb +76 -0
  16. data/app/services/live/order_update_persistence_support.rb +68 -0
  17. data/config/initializers/order_update_hub.rb +16 -0
  18. data/diagram.html +184 -0
  19. data/diagram.md +34 -0
  20. data/docs/rails_integration.md +304 -0
  21. data/exe/DhanHQ +4 -0
  22. data/lib/DhanHQ/client.rb +116 -0
  23. data/lib/DhanHQ/config.rb +32 -0
  24. data/lib/DhanHQ/configuration.rb +72 -0
  25. data/lib/DhanHQ/constants.rb +170 -0
  26. data/lib/DhanHQ/contracts/base_contract.rb +15 -0
  27. data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
  28. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
  29. data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
  30. data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
  31. data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
  32. data/lib/DhanHQ/contracts/order_contract.rb +102 -0
  33. data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
  34. data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
  35. data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
  36. data/lib/DhanHQ/core/base_api.rb +105 -0
  37. data/lib/DhanHQ/core/base_model.rb +266 -0
  38. data/lib/DhanHQ/core/base_resource.rb +50 -0
  39. data/lib/DhanHQ/core/error_handler.rb +19 -0
  40. data/lib/DhanHQ/error_object.rb +49 -0
  41. data/lib/DhanHQ/errors.rb +45 -0
  42. data/lib/DhanHQ/helpers/api_helper.rb +17 -0
  43. data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
  44. data/lib/DhanHQ/helpers/model_helper.rb +7 -0
  45. data/lib/DhanHQ/helpers/request_helper.rb +69 -0
  46. data/lib/DhanHQ/helpers/response_helper.rb +98 -0
  47. data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
  48. data/lib/DhanHQ/json_loader.rb +23 -0
  49. data/lib/DhanHQ/models/edis.rb +58 -0
  50. data/lib/DhanHQ/models/forever_order.rb +85 -0
  51. data/lib/DhanHQ/models/funds.rb +50 -0
  52. data/lib/DhanHQ/models/historical_data.rb +77 -0
  53. data/lib/DhanHQ/models/holding.rb +56 -0
  54. data/lib/DhanHQ/models/kill_switch.rb +49 -0
  55. data/lib/DhanHQ/models/ledger_entry.rb +60 -0
  56. data/lib/DhanHQ/models/margin.rb +54 -0
  57. data/lib/DhanHQ/models/market_feed.rb +41 -0
  58. data/lib/DhanHQ/models/option_chain.rb +79 -0
  59. data/lib/DhanHQ/models/order.rb +239 -0
  60. data/lib/DhanHQ/models/position.rb +60 -0
  61. data/lib/DhanHQ/models/profile.rb +44 -0
  62. data/lib/DhanHQ/models/super_order.rb +69 -0
  63. data/lib/DhanHQ/models/trade.rb +79 -0
  64. data/lib/DhanHQ/rate_limiter.rb +107 -0
  65. data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
  66. data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
  67. data/lib/DhanHQ/requests/orders/create.json +0 -0
  68. data/lib/DhanHQ/resources/edis.rb +44 -0
  69. data/lib/DhanHQ/resources/forever_orders.rb +53 -0
  70. data/lib/DhanHQ/resources/funds.rb +21 -0
  71. data/lib/DhanHQ/resources/historical_data.rb +34 -0
  72. data/lib/DhanHQ/resources/holdings.rb +21 -0
  73. data/lib/DhanHQ/resources/kill_switch.rb +21 -0
  74. data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
  75. data/lib/DhanHQ/resources/market_feed.rb +56 -0
  76. data/lib/DhanHQ/resources/option_chain.rb +31 -0
  77. data/lib/DhanHQ/resources/orders.rb +70 -0
  78. data/lib/DhanHQ/resources/positions.rb +29 -0
  79. data/lib/DhanHQ/resources/profile.rb +25 -0
  80. data/lib/DhanHQ/resources/statements.rb +42 -0
  81. data/lib/DhanHQ/resources/super_orders.rb +46 -0
  82. data/lib/DhanHQ/resources/trades.rb +23 -0
  83. data/lib/DhanHQ/version.rb +6 -0
  84. data/lib/DhanHQ/ws/client.rb +182 -0
  85. data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
  86. data/lib/DhanHQ/ws/connection.rb +240 -0
  87. data/lib/DhanHQ/ws/decoder.rb +83 -0
  88. data/lib/DhanHQ/ws/errors.rb +0 -0
  89. data/lib/DhanHQ/ws/orders/client.rb +59 -0
  90. data/lib/DhanHQ/ws/orders/connection.rb +148 -0
  91. data/lib/DhanHQ/ws/orders.rb +13 -0
  92. data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
  93. data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
  94. data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
  95. data/lib/DhanHQ/ws/packets/header.rb +23 -0
  96. data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
  97. data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
  98. data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
  99. data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
  100. data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
  101. data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
  102. data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
  103. data/lib/DhanHQ/ws/registry.rb +46 -0
  104. data/lib/DhanHQ/ws/segments.rb +75 -0
  105. data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
  106. data/lib/DhanHQ/ws/sub_state.rb +59 -0
  107. data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
  108. data/lib/DhanHQ/ws.rb +37 -0
  109. data/lib/DhanHQ.rb +135 -0
  110. data/lib/ta/technical_analysis.rb +405 -0
  111. data/sig/DhanHQ.rbs +4 -0
  112. data/watchlist.csv +3 -0
  113. metadata +283 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Live
4
+ # Guard-related helpers for interpreting order update payloads.
5
+ module OrderUpdateGuardSupport
6
+ module_function
7
+
8
+ SEGMENT_MAP = {
9
+ %w[NSE E] => "NSE_EQ",
10
+ %w[BSE E] => "BSE_EQ",
11
+ %w[NSE D] => "NSE_FNO",
12
+ %w[BSE D] => "BSE_FNO",
13
+ %w[NSE C] => "NSE_CURRENCY",
14
+ %w[BSE C] => "BSE_CURRENCY",
15
+ %w[MCX M] => "MCX_COMM"
16
+ }.freeze
17
+
18
+ DEFAULT_SL_PCT = 0.15
19
+ DEFAULT_TP_PCT = 0.30
20
+ DEFAULT_TRAIL_PCT = 0.01
21
+
22
+ def map_segment(exchange, segment)
23
+ key = [exchange.to_s.upcase, segment.to_s.upcase]
24
+ SEGMENT_MAP.fetch(key, "NSE_EQ")
25
+ end
26
+
27
+ def position_guard_payload(segment, security_id, order_data)
28
+ guard_base_payload(segment, security_id, order_data)
29
+ .merge(guard_percentage_payload(segment, security_id))
30
+ end
31
+
32
+ def guard_base_payload(segment, security_id, order_data)
33
+ {
34
+ pos_id: nil,
35
+ exchange_segment: segment,
36
+ security_id: security_id,
37
+ entry: average_entry(order_data),
38
+ qty: order_data[:TradedQty].to_i,
39
+ placed_as: placed_as(order_data),
40
+ super_order_id: order_data[:OrderNo].to_s
41
+ }
42
+ end
43
+
44
+ def guard_percentage_payload(segment, security_id)
45
+ {
46
+ sl_pct: default_sl_pct(segment, security_id),
47
+ tp_pct: default_tp_pct(segment, security_id),
48
+ trail_pct: default_trail_pct(segment, security_id)
49
+ }
50
+ end
51
+
52
+ def placed_as(order_data)
53
+ order_data[:Remarks].to_s.match?(/Super Order/i) ? "super" : "plain"
54
+ end
55
+
56
+ def average_entry(order_data)
57
+ first_price = [order_data[:AvgTradedPrice], order_data[:TradedPrice], order_data[:Price]]
58
+ .compact
59
+ .first
60
+ first_price.to_f
61
+ end
62
+
63
+ def default_sl_pct(_segment, _security_id)
64
+ DEFAULT_SL_PCT
65
+ end
66
+
67
+ def default_tp_pct(_segment, _security_id)
68
+ DEFAULT_TP_PCT
69
+ end
70
+
71
+ def default_trail_pct(_segment, _security_id)
72
+ DEFAULT_TRAIL_PCT
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require_relative "order_update_guard_support"
5
+ require_relative "order_update_persistence_support"
6
+
7
+ module Live
8
+ # OrderUpdateHub listens for order updates over WebSocket and wires them into
9
+ # local persistence plus downstream execution helpers.
10
+ class OrderUpdateHub
11
+ include Singleton
12
+
13
+ def start!
14
+ return self if @started
15
+
16
+ @client = DhanHQ::WS::Orders::Client.new.start
17
+ @client.on(:update) { |msg| handle(msg) }
18
+ @started = true
19
+ self
20
+ end
21
+
22
+ def stop!
23
+ @client&.stop
24
+ @started = false
25
+ end
26
+
27
+ private
28
+
29
+ def handle(message)
30
+ return unless order_alert?(message)
31
+
32
+ order_data = message[:Data] || {}
33
+ upsert_local_order(order_data)
34
+ handle_entry_leg(order_data)
35
+ rescue StandardError => e
36
+ Rails.logger.error("[OrderUpdateHub] #{e.class}: #{e.message}")
37
+ end
38
+
39
+ def order_alert?(message)
40
+ message&.dig(:Type) == "order_alert"
41
+ end
42
+
43
+ def handle_entry_leg(order_data)
44
+ return unless entry_leg_traded?(order_data)
45
+
46
+ segment = OrderUpdateGuardSupport.map_segment(order_data[:Exchange], order_data[:Segment])
47
+ security_id = order_data[:SecurityId].to_s
48
+ Live::WsHub.instance.subscribe(seg: segment, sid: security_id) if defined?(Live::WsHub)
49
+
50
+ register_position_guard(segment, security_id, order_data)
51
+ end
52
+
53
+ def register_position_guard(segment, security_id, order_data)
54
+ return unless defined?(Execution::PositionGuard)
55
+
56
+ payload = OrderUpdateGuardSupport.position_guard_payload(segment, security_id, order_data)
57
+ Execution::PositionGuard.instance.register(**payload)
58
+ end
59
+
60
+ def upsert_local_order(order_data)
61
+ order_number = order_data[:OrderNo].to_s
62
+ record = BrokerOrder.find_or_initialize_by(order_no: order_number)
63
+ attributes = OrderUpdatePersistenceSupport.local_order_attributes(order_data)
64
+ record.assign_attributes(attributes)
65
+ record.save!
66
+ rescue NameError
67
+ nil
68
+ end
69
+
70
+ def entry_leg_traded?(order_data)
71
+ order_data[:LegNo].to_i == 1 &&
72
+ order_data[:Status].to_s.upcase == "TRADED" &&
73
+ order_data[:TradedQty].to_i.positive?
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+
5
+ module Live
6
+ # Persistence helper routines for order update payloads.
7
+ module OrderUpdatePersistenceSupport
8
+ module_function
9
+
10
+ def local_order_attributes(order_data)
11
+ identity_attributes(order_data)
12
+ .merge(quantity_attributes(order_data))
13
+ .merge(price_attributes(order_data))
14
+ .merge(timestamp_attributes(order_data))
15
+ end
16
+
17
+ def identity_attributes(order_data)
18
+ {
19
+ exch_order_no: order_data[:ExchOrderNo].to_s,
20
+ status: order_data[:Status],
21
+ product: order_data[:ProductName] || order_data[:Product],
22
+ txn_type: order_data[:TxnType],
23
+ order_type: order_data[:OrderType]
24
+ }
25
+ end
26
+
27
+ def quantity_attributes(order_data)
28
+ {
29
+ validity: order_data[:Validity],
30
+ exchange: order_data[:Exchange],
31
+ segment: order_data[:Segment],
32
+ security_id: order_data[:SecurityId].to_s,
33
+ quantity: order_data[:Quantity].to_i,
34
+ traded_qty: order_data[:TradedQty].to_i
35
+ }
36
+ end
37
+
38
+ def price_attributes(order_data)
39
+ {
40
+ price: decimal(order_data[:Price]),
41
+ trigger_price: decimal(order_data[:TriggerPrice]),
42
+ traded_price: decimal(order_data[:TradedPrice]),
43
+ avg_traded_price: decimal(order_data[:AvgTradedPrice])
44
+ }
45
+ end
46
+
47
+ def timestamp_attributes(order_data)
48
+ {
49
+ last_update_at: parse_timestamp(order_data[:LastUpdatedTime]),
50
+ raw_payload: order_data
51
+ }
52
+ end
53
+
54
+ def decimal(value)
55
+ return BigDecimal(0) if value.nil?
56
+
57
+ BigDecimal(value.to_s)
58
+ end
59
+
60
+ def parse_timestamp(timestamp)
61
+ return nil if timestamp.nil? || timestamp == ""
62
+
63
+ Time.zone.parse(timestamp)
64
+ rescue StandardError
65
+ nil
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV["ENABLE_WS"] == "true"
4
+ Rails.application.config.to_prepare do
5
+ Live::OrderUpdateHub.instance.start!
6
+ Rails.logger.info("[init] Live::OrderUpdateHub started")
7
+ rescue StandardError => e
8
+ Rails.logger.error("[init] OrderUpdateHub failed: #{e.class} #{e.message}")
9
+ end
10
+
11
+ at_exit do
12
+ Live::OrderUpdateHub.instance.stop!
13
+ rescue StandardError => e
14
+ Rails.logger.warn("[exit] OrderUpdateHub stop failed: #{e.message}")
15
+ end
16
+ end