DhanHQ 3.1.0 → 3.2.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/ARCHITECTURE.md +47 -1
- data/CHANGELOG.md +73 -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/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +105 -30
- 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 +6 -3
- 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/risk/pipeline.rb +0 -2
- 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/dhan_hq.rb +4 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
- data/skills/dhanhq-ruby/SKILL.md +2 -1
- data/skills/dhanhq-ruby/examples/order_management.rb +7 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +8 -8
- data/skills/dhanhq-ruby/references/instruments.md +7 -1
- data/skills/dhanhq-ruby/references/orders.md +2 -0
- metadata +30 -10
- data/.rspec +0 -3
- data/.rubocop.yml +0 -50
- data/.rubocop_todo.yml +0 -217
- data/Rakefile +0 -14
- data/TAGS +0 -10
- data/core +0 -0
- data/diagram.html +0 -184
- data/watchlist.csv +0 -3
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
##
|
|
7
|
+
# Margin requirement for a prospective Global Stocks order.
|
|
8
|
+
#
|
|
9
|
+
# @example Check affordability before placing
|
|
10
|
+
# margin = DhanHQ::Models::GlobalStocks::Margin.calculate(
|
|
11
|
+
# security_id: "AAPL", transaction_type: "BUY", price: 180.0, quantity: 10
|
|
12
|
+
# )
|
|
13
|
+
# puts "Need $#{margin.total_margin}, have $#{margin.available_bal}"
|
|
14
|
+
# raise "Insufficient funds" unless margin.sufficient?
|
|
15
|
+
#
|
|
16
|
+
class Margin < BaseModel
|
|
17
|
+
HTTP_PATH = "/v2/globalstocks/margincalculator"
|
|
18
|
+
|
|
19
|
+
attributes :insufficient_bal, :brokerage, :leverage, :total_margin, :available_bal
|
|
20
|
+
|
|
21
|
+
# Returns a concise prompt-friendly summary of the margin requirement.
|
|
22
|
+
def to_prompt
|
|
23
|
+
parts = []
|
|
24
|
+
parts << "total_margin=$#{total_margin}" if total_margin
|
|
25
|
+
parts << "available=$#{available_bal}" if available_bal
|
|
26
|
+
parts << "brokerage=$#{brokerage}" if brokerage
|
|
27
|
+
parts << "leverage=#{leverage}" if leverage
|
|
28
|
+
parts << "shortfall=$#{insufficient_bal}" if insufficient_bal.to_f.positive?
|
|
29
|
+
parts.join(", ")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] True when the account can fund the order.
|
|
33
|
+
def sufficient?
|
|
34
|
+
!insufficient_bal.to_f.positive?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# @return [DhanHQ::Resources::GlobalStocks::MarginCalculator]
|
|
39
|
+
def resource
|
|
40
|
+
@resource ||= DhanHQ::Resources::GlobalStocks::MarginCalculator.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Calculates the margin required for a Global Stocks order.
|
|
44
|
+
#
|
|
45
|
+
# @param params [Hash] `:security_id`, `:transaction_type`, `:price`, `:quantity`
|
|
46
|
+
# @return [Margin]
|
|
47
|
+
def calculate(params)
|
|
48
|
+
new(resource.calculate(params), skip_validation: true)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
module GlobalStocks
|
|
6
|
+
##
|
|
7
|
+
# Current US market session status.
|
|
8
|
+
#
|
|
9
|
+
# @example Gate a strategy on the US session
|
|
10
|
+
# status = DhanHQ::Models::GlobalStocks::MarketStatus.fetch
|
|
11
|
+
# return unless status.open?
|
|
12
|
+
#
|
|
13
|
+
class MarketStatus < BaseModel
|
|
14
|
+
HTTP_PATH = "/v2/globalstocks/marketstatus"
|
|
15
|
+
|
|
16
|
+
attributes :status, :market_open_time, :market_close_time, :holiday_flag
|
|
17
|
+
|
|
18
|
+
# Returns a concise prompt-friendly summary of the session.
|
|
19
|
+
def to_prompt
|
|
20
|
+
parts = ["US market #{status}"]
|
|
21
|
+
parts << "open=#{market_open_time}" if market_open_time
|
|
22
|
+
parts << "close=#{market_close_time}" if market_close_time
|
|
23
|
+
parts << "holiday" if holiday?
|
|
24
|
+
parts.join(", ")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] True when the US market is currently open.
|
|
28
|
+
def open?
|
|
29
|
+
status.to_s.casecmp(DhanHQ::Constants::GlobalStocks::MarketStatus::OPEN).zero?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] True when the US market is currently closed.
|
|
33
|
+
def closed?
|
|
34
|
+
!open?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Boolean] True when today is a US market holiday.
|
|
38
|
+
def holiday?
|
|
39
|
+
holiday_flag == true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# @return [DhanHQ::Resources::GlobalStocks::MarketStatus]
|
|
44
|
+
def resource
|
|
45
|
+
@resource ||= DhanHQ::Resources::GlobalStocks::MarketStatus.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Fetches the current US market status.
|
|
49
|
+
#
|
|
50
|
+
# @return [MarketStatus]
|
|
51
|
+
def fetch
|
|
52
|
+
new(resource.fetch, skip_validation: true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Boolean] True when the US market is currently open.
|
|
56
|
+
def open?
|
|
57
|
+
fetch.open?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
# Typed models for the Global Stocks (US equities) APIs.
|
|
6
|
+
#
|
|
7
|
+
# Global Stocks are a separate book from domestic NSE/BSE trading: separate
|
|
8
|
+
# order book, holdings, trades and a USD fund limit. They are namespaced so
|
|
9
|
+
# `Models::GlobalStocks::Holding` can never be confused with the domestic
|
|
10
|
+
# {DhanHQ::Models::Holding}.
|
|
11
|
+
module GlobalStocks
|
|
12
|
+
##
|
|
13
|
+
# A single Global Stocks (US equity) order.
|
|
14
|
+
#
|
|
15
|
+
# @example Place a limit order for one share of AAPL
|
|
16
|
+
# order = DhanHQ::Models::GlobalStocks::Order.place(
|
|
17
|
+
# transaction_type: "BUY",
|
|
18
|
+
# order_type: "LIMIT",
|
|
19
|
+
# security_id: "AAPL",
|
|
20
|
+
# quantity: 1,
|
|
21
|
+
# price: 180.0
|
|
22
|
+
# )
|
|
23
|
+
# order.order_id # => "112111182198"
|
|
24
|
+
#
|
|
25
|
+
# @example Place a notional ($ value) order
|
|
26
|
+
# DhanHQ::Models::GlobalStocks::Order.place(
|
|
27
|
+
# transaction_type: "BUY",
|
|
28
|
+
# order_type: "AMOUNT",
|
|
29
|
+
# security_id: "AAPL",
|
|
30
|
+
# amount: 500.0
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# @example Fetch the order book
|
|
34
|
+
# DhanHQ::Models::GlobalStocks::Order.all.each do |o|
|
|
35
|
+
# puts "#{o.trading_symbol} #{o.order_status} #{o.traded_qty}/#{o.quantity}"
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
class Order < BaseModel
|
|
39
|
+
HTTP_PATH = "/v2/globalstocks/orders"
|
|
40
|
+
|
|
41
|
+
attributes :dhan_client_id, :order_id, :exchange_order_id, :correlation_id,
|
|
42
|
+
:transaction_type, :exchange_segment, :product_type, :order_type,
|
|
43
|
+
:validity, :trading_symbol, :display_name, :security_id,
|
|
44
|
+
:quantity, :remaining_quantity, :traded_qty, :price, :trigger_price,
|
|
45
|
+
:avg_traded_price, :order_status, :create_time, :exchange_time,
|
|
46
|
+
:update_time, :oms_error_code, :oms_error_description,
|
|
47
|
+
:after_market_order, :amount, :lot_size, :fractional_flag,
|
|
48
|
+
:leg_name, :child_orders
|
|
49
|
+
|
|
50
|
+
# Returns a concise prompt-friendly summary of the order.
|
|
51
|
+
def to_prompt
|
|
52
|
+
parts = ["#{transaction_type} #{quantity || amount}x #{trading_symbol || security_id}"]
|
|
53
|
+
parts << "type=#{order_type}"
|
|
54
|
+
parts << "price=$#{price}" if price&.positive?
|
|
55
|
+
parts << "status=#{order_status}" if order_status
|
|
56
|
+
parts << "traded=#{traded_qty}" if traded_qty&.positive?
|
|
57
|
+
parts << "avg=$#{avg_traded_price}" if avg_traded_price&.positive?
|
|
58
|
+
parts << "error=#{oms_error_description}" if oms_error_description
|
|
59
|
+
parts.join(", ")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Boolean] True when the order can still be modified or cancelled.
|
|
63
|
+
def pending?
|
|
64
|
+
[DhanHQ::Constants::OrderStatus::PENDING,
|
|
65
|
+
DhanHQ::Constants::OrderStatus::TRANSIT,
|
|
66
|
+
DhanHQ::Constants::OrderStatus::PART_TRADED].include?(order_status)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @return [Boolean] True when the order is fully executed.
|
|
70
|
+
def traded?
|
|
71
|
+
order_status == DhanHQ::Constants::OrderStatus::TRADED
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @return [Boolean] True when the order reached a terminal state.
|
|
75
|
+
def closed?
|
|
76
|
+
[DhanHQ::Constants::OrderStatus::TRADED,
|
|
77
|
+
DhanHQ::Constants::OrderStatus::CANCELLED,
|
|
78
|
+
DhanHQ::Constants::OrderStatus::REJECTED,
|
|
79
|
+
DhanHQ::Constants::OrderStatus::EXPIRED].include?(order_status)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] True for a notional (dollar-value) order.
|
|
83
|
+
def notional?
|
|
84
|
+
order_type == DhanHQ::Constants::GlobalStocks::OrderType::AMOUNT
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] True when the order is for a fractional share quantity.
|
|
88
|
+
def fractional?
|
|
89
|
+
return true if fractional_flag
|
|
90
|
+
|
|
91
|
+
quantity.is_a?(Float) && (quantity % 1 != 0)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Child legs of a Global Stocks super order, as {Order} instances.
|
|
95
|
+
#
|
|
96
|
+
# @return [Array<Order>]
|
|
97
|
+
def child_legs
|
|
98
|
+
Array(child_orders).map { |leg| self.class.new(leg, skip_validation: true) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Modifies this order in place and returns the refreshed record.
|
|
102
|
+
#
|
|
103
|
+
# @param params [Hash] Fields to change, in snake_case.
|
|
104
|
+
# @return [Order, DhanHQ::ErrorObject]
|
|
105
|
+
def modify(params = {})
|
|
106
|
+
payload = { transaction_type: transaction_type, order_type: order_type, security_id: security_id }
|
|
107
|
+
.merge(params)
|
|
108
|
+
response = self.class.resource.update(order_id, payload)
|
|
109
|
+
return DhanHQ::ErrorObject.new(response) unless self.class.send(:successful?, response)
|
|
110
|
+
|
|
111
|
+
self.class.find(order_id)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Cancels this order.
|
|
115
|
+
#
|
|
116
|
+
# @return [Boolean] True when the API confirms cancellation.
|
|
117
|
+
def cancel
|
|
118
|
+
response = self.class.resource.cancel(order_id)
|
|
119
|
+
status = response.is_a?(Hash) ? (response[:orderStatus] || response[:order_status]) : nil
|
|
120
|
+
cancelled = status.to_s == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
121
|
+
DhanHQ.logger&.error("[#{self.class.name}] Cancel failed for order #{order_id}: #{response}") unless cancelled
|
|
122
|
+
cancelled
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class << self
|
|
126
|
+
# @return [DhanHQ::Resources::GlobalStocks::Orders]
|
|
127
|
+
def resource
|
|
128
|
+
@resource ||= DhanHQ::Resources::GlobalStocks::Orders.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Retrieves the current trading day's Global Stocks order book.
|
|
132
|
+
#
|
|
133
|
+
# @return [Array<Order>]
|
|
134
|
+
def all
|
|
135
|
+
response = resource.all
|
|
136
|
+
return [] unless response.is_a?(Array)
|
|
137
|
+
|
|
138
|
+
response.map { |order| new(order, skip_validation: true) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Finds a single Global Stocks order by its Dhan order id.
|
|
142
|
+
#
|
|
143
|
+
# @param order_id [String]
|
|
144
|
+
# @return [Order, nil]
|
|
145
|
+
def find(order_id)
|
|
146
|
+
response = resource.find(order_id)
|
|
147
|
+
payload = response.is_a?(Array) ? response.first : response
|
|
148
|
+
return nil unless payload.is_a?(Hash) && !payload.empty?
|
|
149
|
+
|
|
150
|
+
new(payload, skip_validation: true)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Places a new Global Stocks order.
|
|
154
|
+
#
|
|
155
|
+
# @param params [Hash] See {DhanHQ::Contracts::GlobalStocksPlaceOrderContract}.
|
|
156
|
+
# @return [Order, DhanHQ::ErrorObject] The placed order, refetched so the
|
|
157
|
+
# caller gets the full record rather than just `{ orderId:, orderStatus: }`.
|
|
158
|
+
def place(params)
|
|
159
|
+
DhanHQ.logger&.info("[#{name}] Placing global order: #{params}")
|
|
160
|
+
response = resource.create(params)
|
|
161
|
+
return DhanHQ::ErrorObject.new(response) unless successful?(response)
|
|
162
|
+
|
|
163
|
+
id = response[:orderId] || response[:order_id]
|
|
164
|
+
DhanHQ.logger&.info("[#{name}] Global order placed successfully: #{id}")
|
|
165
|
+
find(id) || new(response, skip_validation: true)
|
|
166
|
+
end
|
|
167
|
+
alias create place
|
|
168
|
+
|
|
169
|
+
# Cancels a Global Stocks order by id.
|
|
170
|
+
#
|
|
171
|
+
# @param order_id [String]
|
|
172
|
+
# @return [Boolean]
|
|
173
|
+
def cancel(order_id)
|
|
174
|
+
response = resource.cancel(order_id)
|
|
175
|
+
status = response.is_a?(Hash) ? (response[:orderStatus] || response[:order_status]) : nil
|
|
176
|
+
status.to_s == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
# An order write succeeded when the API echoed an order id back.
|
|
182
|
+
def successful?(response)
|
|
183
|
+
response.is_a?(Hash) && !(response[:orderId] || response[:order_id]).nil?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -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
|