DhanHQ 3.1.0 → 3.2.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 +4 -4
- data/ARCHITECTURE.md +47 -1
- data/CHANGELOG.md +47 -0
- data/README.md +241 -3
- data/docs/AUTHENTICATION.md +3 -5
- data/docs/CONFIGURATION.md +3 -2
- data/docs/CONSTANTS_REFERENCE.md +5 -4
- data/docs/ENDPOINTS_AND_SANDBOX.md +1 -0
- data/docs/LIVE_ORDER_UPDATES.md +5 -10
- data/docs/RAILS_INTEGRATION.md +1 -1
- data/docs/RELEASE_GUIDE.md +13 -2
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +2 -2
- data/docs/WEBSOCKET_INTEGRATION.md +13 -20
- data/docs/WEBSOCKET_PROTOCOL.md +7 -3
- data/lib/DhanHQ/agent/key_coercion.rb +36 -0
- data/lib/DhanHQ/agent/tool.rb +37 -0
- data/lib/DhanHQ/agent/tool_catalogue.rb +180 -0
- data/lib/DhanHQ/agent/tool_handlers.rb +116 -0
- data/lib/DhanHQ/agent/tool_registry.rb +17 -261
- data/lib/DhanHQ/agent/tool_schemas.rb +160 -0
- data/lib/DhanHQ/client.rb +58 -2
- data/lib/DhanHQ/concerns/order_audit.rb +35 -4
- data/lib/DhanHQ/configuration.rb +70 -1
- data/lib/DhanHQ/constants.rb +101 -0
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +1 -9
- data/lib/DhanHQ/contracts/global_stocks_estimator_contract.rb +28 -0
- data/lib/DhanHQ/contracts/global_stocks_modify_order_contract.rb +26 -0
- data/lib/DhanHQ/contracts/global_stocks_order_contract.rb +33 -0
- data/lib/DhanHQ/contracts/global_stocks_place_order_contract.rb +75 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +1 -21
- data/lib/DhanHQ/contracts/multi_order_contract.rb +74 -0
- data/lib/DhanHQ/contracts/trade_history_contract.rb +1 -8
- data/lib/DhanHQ/dry_run/ledger.rb +71 -0
- data/lib/DhanHQ/dry_run/simulator.rb +140 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +23 -0
- data/lib/DhanHQ/models/alert_order.rb +5 -2
- data/lib/DhanHQ/models/global_stocks/funds.rb +56 -0
- data/lib/DhanHQ/models/global_stocks/holding.rb +101 -0
- data/lib/DhanHQ/models/global_stocks/margin.rb +54 -0
- data/lib/DhanHQ/models/global_stocks/market_status.rb +63 -0
- data/lib/DhanHQ/models/global_stocks/order.rb +189 -0
- data/lib/DhanHQ/models/global_stocks/order_estimate.rb +61 -0
- data/lib/DhanHQ/models/global_stocks/trade.rb +74 -0
- data/lib/DhanHQ/models/margin.rb +5 -1
- data/lib/DhanHQ/models/multi_order.rb +130 -0
- data/lib/DhanHQ/resources/global_stocks/funds.rb +25 -0
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +70 -0
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/orders.rb +112 -0
- data/lib/DhanHQ/resources/global_stocks/trades.rb +31 -0
- data/lib/DhanHQ/resources/multi_orders.rb +58 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/write_paths.rb +57 -0
- data/lib/DhanHQ/ws/client.rb +117 -2
- data/lib/DhanHQ/ws/connection.rb +40 -11
- data/lib/DhanHQ/ws/sub_state.rb +14 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
- metadata +30 -40
- data/.rspec +0 -3
- data/.rubocop.yml +0 -50
- data/.rubocop_todo.yml +0 -217
- data/AGENTS.md +0 -23
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -14
- data/TAGS +0 -10
- data/core +0 -0
- data/diagram.html +0 -184
- data/skills/dhanhq-ruby/SKILL.md +0 -207
- data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +0 -54
- data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +0 -65
- data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +0 -89
- data/skills/dhanhq-ruby/examples/iron_condor.rb +0 -137
- data/skills/dhanhq-ruby/examples/live_feed_setup.rb +0 -43
- data/skills/dhanhq-ruby/examples/margin_check.rb +0 -42
- data/skills/dhanhq-ruby/examples/order_management.rb +0 -105
- data/skills/dhanhq-ruby/examples/place_equity_order.rb +0 -36
- data/skills/dhanhq-ruby/examples/place_fno_order.rb +0 -76
- data/skills/dhanhq-ruby/examples/portfolio_summary.rb +0 -74
- data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +0 -57
- data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +0 -65
- data/skills/dhanhq-ruby/references/common-workflows.md +0 -76
- data/skills/dhanhq-ruby/references/error-codes.md +0 -50
- data/skills/dhanhq-ruby/references/funds.md +0 -67
- data/skills/dhanhq-ruby/references/instruments.md +0 -85
- data/skills/dhanhq-ruby/references/live-feed.md +0 -83
- data/skills/dhanhq-ruby/references/market-data.md +0 -119
- data/skills/dhanhq-ruby/references/option-chain.md +0 -71
- data/skills/dhanhq-ruby/references/options-analysis-patterns.md +0 -76
- data/skills/dhanhq-ruby/references/orders.md +0 -201
- data/skills/dhanhq-ruby/references/portfolio.md +0 -93
- data/skills/dhanhq-ruby/references/scanx-data.md +0 -62
- data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +0 -323
- data/skills/dhanhq-ruby/scripts/resolve_security.rb +0 -168
- data/skills/dhanhq-ruby/scripts/trade_logger.rb +0 -131
- data/skills/dhanhq-ruby/scripts/validate_order.rb +0 -169
- data/watchlist.csv +0 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
##
|
|
7
|
+
# Charge estimate for a prospective Global Stocks order.
|
|
8
|
+
#
|
|
9
|
+
# @example Estimate the cost of a trade before placing it
|
|
10
|
+
# est = DhanHQ::Models::GlobalStocks::OrderEstimate.calculate(
|
|
11
|
+
# security_id: "AAPL", transaction_type: "BUY", price: 180.0, quantity: 1
|
|
12
|
+
# )
|
|
13
|
+
# puts "Charges: $#{est.total_charges}"
|
|
14
|
+
#
|
|
15
|
+
class OrderEstimate < BaseModel
|
|
16
|
+
HTTP_PATH = "/v2/globalstocks/transEstimate"
|
|
17
|
+
|
|
18
|
+
attributes :brokerage, :order_charges, :exchange_charges, :turn_over_fee,
|
|
19
|
+
:gst_charges, :other_charges
|
|
20
|
+
|
|
21
|
+
# Returns a concise prompt-friendly summary of the charges.
|
|
22
|
+
def to_prompt
|
|
23
|
+
parts = []
|
|
24
|
+
parts << "brokerage=$#{brokerage}" if brokerage
|
|
25
|
+
parts << "exchange=$#{exchange_charges}" if exchange_charges
|
|
26
|
+
parts << "gst=$#{gst_charges}" if gst_charges
|
|
27
|
+
parts << "total=$#{total_charges}"
|
|
28
|
+
parts.join(", ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Sum of every charge component.
|
|
32
|
+
#
|
|
33
|
+
# The API also returns +order_charges+ as its own total; this method adds up
|
|
34
|
+
# the components so the figure stays correct even when the API omits it.
|
|
35
|
+
#
|
|
36
|
+
# @return [Float]
|
|
37
|
+
def total_charges
|
|
38
|
+
[brokerage, exchange_charges, turn_over_fee, gst_charges, other_charges]
|
|
39
|
+
.sum(&:to_f)
|
|
40
|
+
.round(4)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# @return [DhanHQ::Resources::GlobalStocks::MarginCalculator]
|
|
45
|
+
def resource
|
|
46
|
+
@resource ||= DhanHQ::Resources::GlobalStocks::MarginCalculator.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Estimates the charges applicable to a Global Stocks order.
|
|
50
|
+
#
|
|
51
|
+
# @param params [Hash] `:security_id`, `:transaction_type`, `:price`, `:quantity`
|
|
52
|
+
# @return [OrderEstimate]
|
|
53
|
+
def calculate(params)
|
|
54
|
+
new(resource.estimate(params), skip_validation: true)
|
|
55
|
+
end
|
|
56
|
+
alias estimate calculate
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
##
|
|
7
|
+
# A single executed Global Stocks (US equity) trade.
|
|
8
|
+
#
|
|
9
|
+
# @example Today's US trades
|
|
10
|
+
# DhanHQ::Models::GlobalStocks::Trade.all.each do |t|
|
|
11
|
+
# puts "#{t.trading_symbol} #{t.traded_quantity} @ $#{t.traded_price}"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Trades for one symbol
|
|
15
|
+
# DhanHQ::Models::GlobalStocks::Trade.find_by_security_id("AAPL")
|
|
16
|
+
#
|
|
17
|
+
class Trade < BaseModel
|
|
18
|
+
HTTP_PATH = "/v2/globalstocks/trades"
|
|
19
|
+
|
|
20
|
+
attributes :dhan_client_id, :order_id, :exchange_order_id, :transaction_type,
|
|
21
|
+
:exchange_segment, :product_type, :order_type, :trading_symbol,
|
|
22
|
+
:security_id, :traded_quantity, :traded_price, :trade_date,
|
|
23
|
+
:create_time, :exchange_time, :order_status, :brokerage,
|
|
24
|
+
:other_charges, :trade_value
|
|
25
|
+
|
|
26
|
+
# Returns a concise prompt-friendly summary of the trade.
|
|
27
|
+
def to_prompt
|
|
28
|
+
parts = ["#{transaction_type} #{traded_quantity}x #{trading_symbol || security_id}"]
|
|
29
|
+
parts << "@ $#{traded_price}" if traded_price
|
|
30
|
+
parts << "value=$#{trade_value}" if trade_value
|
|
31
|
+
parts << "on #{trade_date}" if trade_date
|
|
32
|
+
parts.join(", ")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Total cost of the trade, brokerage and other charges combined.
|
|
36
|
+
#
|
|
37
|
+
# @return [Float]
|
|
38
|
+
def total_charges
|
|
39
|
+
brokerage.to_f + other_charges.to_f
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# @return [DhanHQ::Resources::GlobalStocks::Trades]
|
|
44
|
+
def resource
|
|
45
|
+
@resource ||= DhanHQ::Resources::GlobalStocks::Trades.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieves the current trading day's executed Global Stocks trades.
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Trade>]
|
|
51
|
+
def all
|
|
52
|
+
build_collection(resource.all)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Retrieves executed Global Stocks trades for a single security.
|
|
56
|
+
#
|
|
57
|
+
# @param security_id [String]
|
|
58
|
+
# @return [Array<Trade>]
|
|
59
|
+
def find_by_security_id(security_id)
|
|
60
|
+
build_collection(resource.by_security_id(security_id))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_collection(response)
|
|
66
|
+
return [] unless response.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
response.map { |trade| new(trade, skip_validation: true) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/DhanHQ/models/margin.rb
CHANGED
|
@@ -142,7 +142,11 @@ module DhanHQ
|
|
|
142
142
|
# puts "Total margin: #{margin.total_margin}"
|
|
143
143
|
#
|
|
144
144
|
def calculate_multi(params)
|
|
145
|
-
#
|
|
145
|
+
# Work on a copy: callers may pass a frozen or reused hash, and normalising
|
|
146
|
+
# their argument in place is a side effect they cannot see from the signature.
|
|
147
|
+
params = params.dup
|
|
148
|
+
|
|
149
|
+
# Accept either spelling of the collection and order flags.
|
|
146
150
|
params[:scrip_list] ||= params[:scripts] if params.key?(:scripts)
|
|
147
151
|
params[:include_order] ||= params[:include_orders] if params.key?(:include_orders)
|
|
148
152
|
params[:dhan_client_id] ||= DhanHQ.configuration.client_id
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# A basket of up to 15 orders placed in a single request via
|
|
7
|
+
# POST /v2/alerts/multi/orders.
|
|
8
|
+
#
|
|
9
|
+
# Unlike {AlertOrder} there is no trigger condition — every leg goes to the
|
|
10
|
+
# exchange immediately. The API answers with one result per leg, keyed by the
|
|
11
|
+
# +sequence+ you supplied, so partial acceptance is visible.
|
|
12
|
+
#
|
|
13
|
+
# @example Place a two-leg basket
|
|
14
|
+
# result = DhanHQ::Models::MultiOrder.place([
|
|
15
|
+
# { sequence: "1", transaction_type: "BUY", exchange_segment: "NSE_EQ",
|
|
16
|
+
# product_type: "CNC", order_type: "LIMIT", validity: "DAY",
|
|
17
|
+
# security_id: "11536", quantity: 1, price: 1500.0 },
|
|
18
|
+
# { sequence: "2", transaction_type: "BUY", exchange_segment: "NSE_EQ",
|
|
19
|
+
# product_type: "CNC", order_type: "MARKET", validity: "DAY",
|
|
20
|
+
# security_id: "1333", quantity: 2 }
|
|
21
|
+
# ])
|
|
22
|
+
#
|
|
23
|
+
# result.all_accepted? # => true
|
|
24
|
+
# result.rejected # => []
|
|
25
|
+
# result.order_ids # => ["112111182198", "112111182199"]
|
|
26
|
+
# result.for_sequence("1").order_id
|
|
27
|
+
#
|
|
28
|
+
class MultiOrder < BaseModel
|
|
29
|
+
HTTP_PATH = "/v2/alerts/multi/orders"
|
|
30
|
+
|
|
31
|
+
attributes :orders
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# One leg's result within a {MultiOrder} response.
|
|
35
|
+
class Leg < BaseModel
|
|
36
|
+
attributes :order_id, :sequence, :order_status
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] True when the exchange accepted this leg.
|
|
39
|
+
def accepted?
|
|
40
|
+
!order_id.nil? && !rejected?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Boolean] True when this leg was rejected.
|
|
44
|
+
def rejected?
|
|
45
|
+
order_status.to_s == DhanHQ::Constants::OrderStatus::REJECTED
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns a concise prompt-friendly summary of the leg.
|
|
49
|
+
def to_prompt
|
|
50
|
+
"seq=#{sequence} order_id=#{order_id} status=#{order_status}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Per-leg results.
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<Leg>]
|
|
57
|
+
def legs
|
|
58
|
+
@legs ||= Array(orders).map { |leg| Leg.new(leg, skip_validation: true) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Order ids of every leg the API returned one for.
|
|
62
|
+
#
|
|
63
|
+
# @return [Array<String>]
|
|
64
|
+
def order_ids
|
|
65
|
+
legs.filter_map(&:order_id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Legs the exchange accepted.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Leg>]
|
|
71
|
+
def accepted
|
|
72
|
+
legs.select(&:accepted?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Legs the exchange rejected.
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<Leg>]
|
|
78
|
+
def rejected
|
|
79
|
+
legs.reject(&:accepted?)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] True when every leg was accepted.
|
|
83
|
+
def all_accepted?
|
|
84
|
+
legs.any? && rejected.empty?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] True when some but not all legs were accepted.
|
|
88
|
+
def partially_accepted?
|
|
89
|
+
accepted.any? && rejected.any?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Looks up a leg by the sequence supplied at placement.
|
|
93
|
+
#
|
|
94
|
+
# @param sequence [String, Integer]
|
|
95
|
+
# @return [Leg, nil]
|
|
96
|
+
def for_sequence(sequence)
|
|
97
|
+
legs.find { |leg| leg.sequence.to_s == sequence.to_s }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns a concise prompt-friendly summary of the basket.
|
|
101
|
+
def to_prompt
|
|
102
|
+
"#{legs.size} legs, #{accepted.size} accepted, #{rejected.size} rejected"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
# @return [DhanHQ::Resources::MultiOrders]
|
|
107
|
+
def resource
|
|
108
|
+
@resource ||= DhanHQ::Resources::MultiOrders.new
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Places a basket of orders.
|
|
112
|
+
#
|
|
113
|
+
# @param orders [Array<Hash>] Order legs. See {DhanHQ::Contracts::MultiOrderContract}.
|
|
114
|
+
# @param dhan_client_id [String, nil] Defaults to the configured client id.
|
|
115
|
+
# @return [MultiOrder, DhanHQ::ErrorObject]
|
|
116
|
+
def place(orders, dhan_client_id: nil)
|
|
117
|
+
DhanHQ.logger&.info("[#{name}] Placing multi order with #{Array(orders).size} legs")
|
|
118
|
+
response = resource.create(orders, dhan_client_id: dhan_client_id)
|
|
119
|
+
|
|
120
|
+
# The API may answer with a bare array of leg results or wrap it in `orders`.
|
|
121
|
+
payload = response.is_a?(Array) ? { orders: response } : response
|
|
122
|
+
return DhanHQ::ErrorObject.new(response) unless payload.is_a?(Hash) && payload[:orders]
|
|
123
|
+
|
|
124
|
+
new(payload, skip_validation: true)
|
|
125
|
+
end
|
|
126
|
+
alias create place
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
# Resource client for the Global Stocks fund limit.
|
|
7
|
+
#
|
|
8
|
+
# GET /v2/globalstocks/fundlimit
|
|
9
|
+
#
|
|
10
|
+
# Global Stocks funds are held in USD and are reported separately from the
|
|
11
|
+
# domestic INR fund limit exposed by {DhanHQ::Resources::Funds}.
|
|
12
|
+
class Funds < BaseAPI
|
|
13
|
+
API_TYPE = :order_api
|
|
14
|
+
HTTP_PATH = "/v2/globalstocks/fundlimit"
|
|
15
|
+
|
|
16
|
+
# Retrieves the authenticated user's US stock fund limit details.
|
|
17
|
+
#
|
|
18
|
+
# @return [Hash]
|
|
19
|
+
def fetch
|
|
20
|
+
get("")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
# Resource client for Global Stocks holdings.
|
|
7
|
+
#
|
|
8
|
+
# GET /v2/globalstocks/holdings
|
|
9
|
+
class Holdings < BaseAPI
|
|
10
|
+
API_TYPE = :order_api
|
|
11
|
+
HTTP_PATH = "/v2/globalstocks/holdings"
|
|
12
|
+
|
|
13
|
+
# Retrieves the authenticated user's US stock holdings.
|
|
14
|
+
#
|
|
15
|
+
# @return [Array<Hash>]
|
|
16
|
+
def all
|
|
17
|
+
get("")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
# Resource client for the Global Stocks pre-trade calculators.
|
|
7
|
+
#
|
|
8
|
+
# POST /v2/globalstocks/margincalculator — margin required for an order
|
|
9
|
+
# POST /v2/globalstocks/transEstimate — charges applicable to an order
|
|
10
|
+
#
|
|
11
|
+
# Both endpoints take the same request body. The API documents +price+ and
|
|
12
|
+
# +quantity+ as strings, so numeric input is stringified before sending.
|
|
13
|
+
class MarginCalculator < BaseAPI
|
|
14
|
+
API_TYPE = :non_trading_api
|
|
15
|
+
HTTP_PATH = "/v2/globalstocks"
|
|
16
|
+
|
|
17
|
+
# Calculates the margin required for a Global Stocks order.
|
|
18
|
+
#
|
|
19
|
+
# @param params [Hash] `:security_id`, `:transaction_type`, `:price`, `:quantity`
|
|
20
|
+
# @return [Hash] Margin breakdown.
|
|
21
|
+
def calculate(params)
|
|
22
|
+
post("/margincalculator", params: wire_params(params))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Estimates the charges applicable to a Global Stocks order.
|
|
26
|
+
#
|
|
27
|
+
# @param params [Hash] `:security_id`, `:transaction_type`, `:price`, `:quantity`
|
|
28
|
+
# @return [Hash] Charge breakdown.
|
|
29
|
+
def estimate(params)
|
|
30
|
+
post("/transEstimate", params: wire_params(params))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Validates against the shared estimator contract, then renders the numeric
|
|
36
|
+
# fields as strings the way the API expects them.
|
|
37
|
+
def wire_params(params)
|
|
38
|
+
validated = validate!(params)
|
|
39
|
+
validated.merge(
|
|
40
|
+
price: format_number(validated[:price]),
|
|
41
|
+
quantity: format_number(validated[:quantity])
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fields the API documents as strings but which are validated as numbers.
|
|
46
|
+
NUMERIC_KEYS = %i[price quantity].freeze
|
|
47
|
+
private_constant :NUMERIC_KEYS
|
|
48
|
+
|
|
49
|
+
def validate!(params)
|
|
50
|
+
attrs = snake_case(params).each_with_object({}) do |(key, value), out|
|
|
51
|
+
out[key] = NUMERIC_KEYS.include?(key) ? Float(value) : value
|
|
52
|
+
end
|
|
53
|
+
result = Contracts::GlobalStocksEstimatorContract.new.call(attrs)
|
|
54
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}" unless result.success?
|
|
55
|
+
|
|
56
|
+
result.to_h
|
|
57
|
+
rescue ArgumentError, TypeError => e
|
|
58
|
+
raise DhanHQ::ValidationError, "Invalid parameters: price and quantity must be numeric (#{e.message})"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Renders 180.0 as "180" and 1.5 as "1.5" so the payload matches the
|
|
62
|
+
# documented examples rather than leaking Ruby float formatting.
|
|
63
|
+
def format_number(value)
|
|
64
|
+
float = Float(value)
|
|
65
|
+
float == float.to_i ? float.to_i.to_s : float.to_s
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
# Resource client for the US market session status.
|
|
7
|
+
#
|
|
8
|
+
# GET /v2/globalstocks/marketstatus
|
|
9
|
+
class MarketStatus < BaseAPI
|
|
10
|
+
API_TYPE = :non_trading_api
|
|
11
|
+
HTTP_PATH = "/v2/globalstocks/marketstatus"
|
|
12
|
+
|
|
13
|
+
# Retrieves the current US market status and session timings.
|
|
14
|
+
#
|
|
15
|
+
# @return [Hash] `{ status:, marketOpenTime:, marketCloseTime:, holidayFlag: }`
|
|
16
|
+
def fetch
|
|
17
|
+
get("")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../concerns/order_audit"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Resources
|
|
7
|
+
module GlobalStocks
|
|
8
|
+
# Resource client for the Global Stocks (US equities) order endpoints.
|
|
9
|
+
#
|
|
10
|
+
# POST /v2/globalstocks/orders
|
|
11
|
+
# PUT /v2/globalstocks/orders/{order-id}
|
|
12
|
+
# DELETE /v2/globalstocks/orders/{order-id}
|
|
13
|
+
# GET /v2/globalstocks/orders
|
|
14
|
+
# GET /v2/globalstocks/orders/{order-id}
|
|
15
|
+
#
|
|
16
|
+
# Writes go through the same {DhanHQ::Concerns::OrderAudit} guardrails as
|
|
17
|
+
# domestic orders: +ENV["LIVE_TRADING"]="true"+ is required, and every attempt
|
|
18
|
+
# emits a structured audit log line.
|
|
19
|
+
#
|
|
20
|
+
# The pre-trade {DhanHQ::Risk::Pipeline} is intentionally *not* run here. Its
|
|
21
|
+
# checks (lot-size multiples, ASM/GSM surveillance lists, F&O product support,
|
|
22
|
+
# NSE/BSE market hours) resolve instruments from the Indian scrip master, which
|
|
23
|
+
# does not contain US securities.
|
|
24
|
+
#
|
|
25
|
+
# @see https://dhanhq.co/docs/v2/ Global Stocks section
|
|
26
|
+
class Orders < BaseAPI
|
|
27
|
+
include DhanHQ::Concerns::OrderAudit
|
|
28
|
+
|
|
29
|
+
API_TYPE = :order_api
|
|
30
|
+
HTTP_PATH = "/v2/globalstocks/orders"
|
|
31
|
+
|
|
32
|
+
# Places a new Global Stocks order.
|
|
33
|
+
#
|
|
34
|
+
# @param params [Hash] Order attributes in snake_case. See
|
|
35
|
+
# {DhanHQ::Contracts::GlobalStocksPlaceOrderContract} for the accepted keys.
|
|
36
|
+
# @return [Hash] `{ orderId:, orderStatus: }`
|
|
37
|
+
# @raise [DhanHQ::LiveTradingDisabledError] Unless LIVE_TRADING is enabled.
|
|
38
|
+
# @raise [DhanHQ::ValidationError] When the payload fails validation.
|
|
39
|
+
def create(params)
|
|
40
|
+
ensure_live_trading!
|
|
41
|
+
log_order_context("DHAN_GLOBAL_ORDER_ATTEMPT", params)
|
|
42
|
+
validate_place_order!(params)
|
|
43
|
+
post("", params: params)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Modifies a pending Global Stocks order.
|
|
47
|
+
#
|
|
48
|
+
# @param order_id [String]
|
|
49
|
+
# @param params [Hash] Fields to change, in snake_case.
|
|
50
|
+
# @return [Hash] `{ orderId:, orderStatus: }`
|
|
51
|
+
def update(order_id, params)
|
|
52
|
+
ensure_live_trading!
|
|
53
|
+
log_order_context("DHAN_GLOBAL_ORDER_MODIFY_ATTEMPT", params.merge(order_id: order_id))
|
|
54
|
+
validate_modify_order!(params.merge(order_id: order_id))
|
|
55
|
+
put("/#{order_id}", params: params)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Cancels a pending Global Stocks order.
|
|
59
|
+
#
|
|
60
|
+
# @param order_id [String]
|
|
61
|
+
# @return [Hash] `{ orderId:, orderStatus: }`
|
|
62
|
+
def cancel(order_id)
|
|
63
|
+
ensure_live_trading!
|
|
64
|
+
log_order_context("DHAN_GLOBAL_ORDER_CANCEL_ATTEMPT", { order_id: order_id })
|
|
65
|
+
delete("/#{order_id}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Retrieves the current trading day's Global Stocks order book.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<Hash>]
|
|
71
|
+
def all
|
|
72
|
+
get("")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Retrieves a single Global Stocks order by its Dhan order id.
|
|
76
|
+
#
|
|
77
|
+
# @param order_id [String]
|
|
78
|
+
# @return [Hash]
|
|
79
|
+
def find(order_id)
|
|
80
|
+
get("/#{order_id}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def validate_place_order!(params)
|
|
86
|
+
result = Contracts::GlobalStocksPlaceOrderContract.new.call(coerce_for_validation(params))
|
|
87
|
+
raise_validation_error!(result) unless result.success?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate_modify_order!(params)
|
|
91
|
+
result = Contracts::GlobalStocksModifyOrderContract.new.call(coerce_for_validation(params))
|
|
92
|
+
raise_validation_error!(result) unless result.success?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Global Stocks quantities and prices are floats (fractional shares are
|
|
96
|
+
# supported), so integers handed in by callers are widened before validation.
|
|
97
|
+
def coerce_for_validation(params)
|
|
98
|
+
snake_case(params).each_with_object({}) do |(key, value), out|
|
|
99
|
+
out[key] = FLOAT_KEYS.include?(key) && value.is_a?(Integer) ? value.to_f : value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
FLOAT_KEYS = %i[quantity price trigger_price stop_loss_price target_price amount].freeze
|
|
104
|
+
private_constant :FLOAT_KEYS
|
|
105
|
+
|
|
106
|
+
def raise_validation_error!(result)
|
|
107
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
# Resource client for the Global Stocks trade book.
|
|
7
|
+
#
|
|
8
|
+
# GET /v2/globalstocks/trades
|
|
9
|
+
# GET /v2/globalstocks/trades/{security-id}
|
|
10
|
+
class Trades < BaseAPI
|
|
11
|
+
API_TYPE = :order_api
|
|
12
|
+
HTTP_PATH = "/v2/globalstocks/trades"
|
|
13
|
+
|
|
14
|
+
# Retrieves the current trading day's executed Global Stocks trades.
|
|
15
|
+
#
|
|
16
|
+
# @return [Array<Hash>]
|
|
17
|
+
def all
|
|
18
|
+
get("")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Retrieves executed Global Stocks trades for a single security.
|
|
22
|
+
#
|
|
23
|
+
# @param security_id [String]
|
|
24
|
+
# @return [Array<Hash>]
|
|
25
|
+
def by_security_id(security_id)
|
|
26
|
+
get("/#{security_id}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../concerns/order_audit"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Resources
|
|
7
|
+
# Resource for basket orders: POST /v2/alerts/multi/orders.
|
|
8
|
+
#
|
|
9
|
+
# Places up to {DhanHQ::Contracts::MultiOrderContract::MAX_ORDERS} unconditional
|
|
10
|
+
# orders in a single request. Unlike {AlertOrders} there is no trigger condition —
|
|
11
|
+
# every leg is sent to the exchange immediately.
|
|
12
|
+
#
|
|
13
|
+
# @see https://dhanhq.co/docs/v2/ Conditional and Multi Order section
|
|
14
|
+
class MultiOrders < BaseAPI
|
|
15
|
+
include DhanHQ::Concerns::OrderAudit
|
|
16
|
+
|
|
17
|
+
API_TYPE = :order_api
|
|
18
|
+
HTTP_PATH = "/v2/alerts/multi/orders"
|
|
19
|
+
|
|
20
|
+
# Places a basket of orders.
|
|
21
|
+
#
|
|
22
|
+
# @param orders [Array<Hash>] Order legs in snake_case. Each leg requires
|
|
23
|
+
# +:sequence+, +:transaction_type+ and +:exchange_segment+.
|
|
24
|
+
# @param dhan_client_id [String, nil] Defaults to the configured client id.
|
|
25
|
+
# @return [Hash] `{ orders: [{ orderId:, sequence:, orderStatus: }, ...] }`
|
|
26
|
+
# @raise [DhanHQ::LiveTradingDisabledError] Unless LIVE_TRADING is enabled.
|
|
27
|
+
# @raise [DhanHQ::ValidationError] When any leg fails validation.
|
|
28
|
+
def create(orders, dhan_client_id: nil)
|
|
29
|
+
ensure_live_trading!
|
|
30
|
+
|
|
31
|
+
legs = Array(orders).map { |leg| snake_case(leg) }
|
|
32
|
+
payload = { dhan_client_id: dhan_client_id || DhanHQ.configuration&.client_id, orders: legs }.compact
|
|
33
|
+
|
|
34
|
+
validate!(payload)
|
|
35
|
+
legs.each { |leg| run_risk_checks!(leg) }
|
|
36
|
+
log_multi_order_context(legs)
|
|
37
|
+
|
|
38
|
+
# BaseAPI camelizes only the top level, which would leave the leg fields as
|
|
39
|
+
# `transaction_type`/`security_id` inside `orders` and get the basket rejected.
|
|
40
|
+
post("", params: payload.merge(orders: deep_camelize_keys(legs)))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def validate!(payload)
|
|
46
|
+
result = Contracts::MultiOrderContract.new.call(payload)
|
|
47
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}" unless result.success?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Emits one audit line per leg so each order in the basket is traceable.
|
|
51
|
+
def log_multi_order_context(legs)
|
|
52
|
+
legs.each do |leg|
|
|
53
|
+
log_order_context("DHAN_MULTI_ORDER_ATTEMPT", leg)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
# Classifies request paths by whether they change account state.
|
|
5
|
+
#
|
|
6
|
+
# The distinction that matters on this API is **mutating vs read-only**, not GET vs
|
|
7
|
+
# POST: the option chain, market feed, historical charts and the margin calculators
|
|
8
|
+
# are all read-only POSTs. Both dry-run simulation and non-idempotent retry gating
|
|
9
|
+
# need that answer, so it lives here rather than in {DhanHQ::Client}.
|
|
10
|
+
module WritePaths
|
|
11
|
+
# HTTP methods that can change server-side state.
|
|
12
|
+
WRITE_METHODS = %i[post put patch delete].freeze
|
|
13
|
+
|
|
14
|
+
# Path of the basket order endpoint, which answers with one result per leg
|
|
15
|
+
# rather than a single order id.
|
|
16
|
+
MULTI_ORDER_PATH = "/v2/alerts/multi/orders"
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# True when this request changes real account state.
|
|
21
|
+
#
|
|
22
|
+
# @param method [Symbol] HTTP method
|
|
23
|
+
# @param path [String, nil] Request path
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def mutating?(method, path)
|
|
26
|
+
return false unless WRITE_METHODS.include?(method)
|
|
27
|
+
|
|
28
|
+
match?(path, DhanHQ::Constants::MUTATING_PATH_PREFIXES)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# True when this request places one or more orders.
|
|
32
|
+
#
|
|
33
|
+
# @param method [Symbol] HTTP method
|
|
34
|
+
# @param path [String, nil] Request path
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def order_placement?(method, path)
|
|
37
|
+
return false unless method == :post
|
|
38
|
+
|
|
39
|
+
match?(path, DhanHQ::Constants::ORDER_PLACEMENT_PATH_PREFIXES)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True for the basket order endpoint.
|
|
43
|
+
#
|
|
44
|
+
# @param path [String, nil] Request path
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def multi_order?(path)
|
|
47
|
+
path.to_s.start_with?(MULTI_ORDER_PATH)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def match?(path, prefixes)
|
|
51
|
+
return false if path.nil?
|
|
52
|
+
|
|
53
|
+
prefixes.any? { |prefix| path.start_with?(prefix) }
|
|
54
|
+
end
|
|
55
|
+
private_class_method :match?
|
|
56
|
+
end
|
|
57
|
+
end
|