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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +20 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +555 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/README1.md +521 -0
- data/Rakefile +12 -0
- data/TAGS +10 -0
- data/TODO-1.md +14 -0
- data/TODO.md +127 -0
- data/app/services/live/order_update_guard_support.rb +75 -0
- data/app/services/live/order_update_hub.rb +76 -0
- data/app/services/live/order_update_persistence_support.rb +68 -0
- data/config/initializers/order_update_hub.rb +16 -0
- data/diagram.html +184 -0
- data/diagram.md +34 -0
- data/docs/rails_integration.md +304 -0
- data/exe/DhanHQ +4 -0
- data/lib/DhanHQ/client.rb +116 -0
- data/lib/DhanHQ/config.rb +32 -0
- data/lib/DhanHQ/configuration.rb +72 -0
- data/lib/DhanHQ/constants.rb +170 -0
- data/lib/DhanHQ/contracts/base_contract.rb +15 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
- data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
- data/lib/DhanHQ/contracts/order_contract.rb +102 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
- data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
- data/lib/DhanHQ/core/base_api.rb +105 -0
- data/lib/DhanHQ/core/base_model.rb +266 -0
- data/lib/DhanHQ/core/base_resource.rb +50 -0
- data/lib/DhanHQ/core/error_handler.rb +19 -0
- data/lib/DhanHQ/error_object.rb +49 -0
- data/lib/DhanHQ/errors.rb +45 -0
- data/lib/DhanHQ/helpers/api_helper.rb +17 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
- data/lib/DhanHQ/helpers/model_helper.rb +7 -0
- data/lib/DhanHQ/helpers/request_helper.rb +69 -0
- data/lib/DhanHQ/helpers/response_helper.rb +98 -0
- data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
- data/lib/DhanHQ/json_loader.rb +23 -0
- data/lib/DhanHQ/models/edis.rb +58 -0
- data/lib/DhanHQ/models/forever_order.rb +85 -0
- data/lib/DhanHQ/models/funds.rb +50 -0
- data/lib/DhanHQ/models/historical_data.rb +77 -0
- data/lib/DhanHQ/models/holding.rb +56 -0
- data/lib/DhanHQ/models/kill_switch.rb +49 -0
- data/lib/DhanHQ/models/ledger_entry.rb +60 -0
- data/lib/DhanHQ/models/margin.rb +54 -0
- data/lib/DhanHQ/models/market_feed.rb +41 -0
- data/lib/DhanHQ/models/option_chain.rb +79 -0
- data/lib/DhanHQ/models/order.rb +239 -0
- data/lib/DhanHQ/models/position.rb +60 -0
- data/lib/DhanHQ/models/profile.rb +44 -0
- data/lib/DhanHQ/models/super_order.rb +69 -0
- data/lib/DhanHQ/models/trade.rb +79 -0
- data/lib/DhanHQ/rate_limiter.rb +107 -0
- data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
- data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
- data/lib/DhanHQ/requests/orders/create.json +0 -0
- data/lib/DhanHQ/resources/edis.rb +44 -0
- data/lib/DhanHQ/resources/forever_orders.rb +53 -0
- data/lib/DhanHQ/resources/funds.rb +21 -0
- data/lib/DhanHQ/resources/historical_data.rb +34 -0
- data/lib/DhanHQ/resources/holdings.rb +21 -0
- data/lib/DhanHQ/resources/kill_switch.rb +21 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
- data/lib/DhanHQ/resources/market_feed.rb +56 -0
- data/lib/DhanHQ/resources/option_chain.rb +31 -0
- data/lib/DhanHQ/resources/orders.rb +70 -0
- data/lib/DhanHQ/resources/positions.rb +29 -0
- data/lib/DhanHQ/resources/profile.rb +25 -0
- data/lib/DhanHQ/resources/statements.rb +42 -0
- data/lib/DhanHQ/resources/super_orders.rb +46 -0
- data/lib/DhanHQ/resources/trades.rb +23 -0
- data/lib/DhanHQ/version.rb +6 -0
- data/lib/DhanHQ/ws/client.rb +182 -0
- data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
- data/lib/DhanHQ/ws/connection.rb +240 -0
- data/lib/DhanHQ/ws/decoder.rb +83 -0
- data/lib/DhanHQ/ws/errors.rb +0 -0
- data/lib/DhanHQ/ws/orders/client.rb +59 -0
- data/lib/DhanHQ/ws/orders/connection.rb +148 -0
- data/lib/DhanHQ/ws/orders.rb +13 -0
- data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
- data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
- data/lib/DhanHQ/ws/packets/header.rb +23 -0
- data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
- data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
- data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
- data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
- data/lib/DhanHQ/ws/registry.rb +46 -0
- data/lib/DhanHQ/ws/segments.rb +75 -0
- data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
- data/lib/DhanHQ/ws/sub_state.rb +59 -0
- data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
- data/lib/DhanHQ/ws.rb +37 -0
- data/lib/DhanHQ.rb +135 -0
- data/lib/ta/technical_analysis.rb +405 -0
- data/sig/DhanHQ.rbs +4 -0
- data/watchlist.csv +3 -0
- 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
|