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,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Represents a single row/entry in the Ledger.
|
7
|
+
# Ledger data typically returns an array of these objects.
|
8
|
+
class LedgerEntry < BaseModel
|
9
|
+
# The endpoint is /v2/ledger?from-date=...&to-date=...
|
10
|
+
# So we may define a resource path or rely on the Statements resource.
|
11
|
+
HTTP_PATH = "/v2/ledger"
|
12
|
+
|
13
|
+
# Typical fields from API docs
|
14
|
+
attributes :dhan_client_id, :narration, :voucherdate, :exchange,
|
15
|
+
:voucherdesc, :vouchernumber, :debit, :credit, :runbal
|
16
|
+
|
17
|
+
class << self
|
18
|
+
##
|
19
|
+
# Provides a **shared instance** of the `Statements` resource.
|
20
|
+
#
|
21
|
+
# @return [DhanHQ::Resources::Statements]
|
22
|
+
def resource
|
23
|
+
@resource ||= DhanHQ::Resources::Statements.new
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Fetch ledger entries for the given date range.
|
28
|
+
#
|
29
|
+
# @param from_date [String] e.g. "2023-01-01"
|
30
|
+
# @param to_date [String] e.g. "2023-01-31"
|
31
|
+
# @return [Array<LedgerEntry>]
|
32
|
+
def all(from_date:, to_date:)
|
33
|
+
# The resource call returns an Array<Hash>, according to the docs.
|
34
|
+
response = resource.ledger(from_date: from_date, to_date: to_date)
|
35
|
+
|
36
|
+
return [] unless response.is_a?(Array)
|
37
|
+
|
38
|
+
response.map do |entry|
|
39
|
+
new(entry, skip_validation: true)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Optional: you can override #to_h or #inspect if you want a custom representation
|
45
|
+
def to_h
|
46
|
+
{
|
47
|
+
dhan_client_id: dhan_client_id,
|
48
|
+
narration: narration,
|
49
|
+
voucherdate: voucherdate,
|
50
|
+
exchange: exchange,
|
51
|
+
voucherdesc: voucherdesc,
|
52
|
+
vouchernumber: vouchernumber,
|
53
|
+
debit: debit,
|
54
|
+
credit: credit,
|
55
|
+
runbal: runbal
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
# Model for the on-demand margin calculator response.
|
6
|
+
class Margin < BaseModel
|
7
|
+
# Base path used to invoke the calculator.
|
8
|
+
HTTP_PATH = "/v2/margincalculator"
|
9
|
+
|
10
|
+
attr_reader :total_margin, :span_margin, :exposure_margin, :available_balance,
|
11
|
+
:variable_margin, :insufficient_balance, :brokerage, :leverage
|
12
|
+
|
13
|
+
class << self
|
14
|
+
##
|
15
|
+
# Provides a **shared instance** of the `MarginCalculator` resource.
|
16
|
+
#
|
17
|
+
# @return [DhanHQ::Resources::MarginCalculator]
|
18
|
+
def resource
|
19
|
+
@resource ||= DhanHQ::Resources::MarginCalculator.new
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Calculate margin requirements for an order.
|
24
|
+
#
|
25
|
+
# @param params [Hash] Request parameters for margin calculation.
|
26
|
+
# @return [Margin]
|
27
|
+
def calculate(params)
|
28
|
+
formatted_params = camelize_keys(params)
|
29
|
+
validate_params!(formatted_params, DhanHQ::Contracts::MarginCalculatorContract)
|
30
|
+
|
31
|
+
response = resource.calculate(formatted_params)
|
32
|
+
new(response, skip_validation: true)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Convert model attributes to a hash.
|
38
|
+
#
|
39
|
+
# @return [Hash] Hash representation of the Margin model.
|
40
|
+
def to_h
|
41
|
+
{
|
42
|
+
total_margin: total_margin,
|
43
|
+
span_margin: span_margin,
|
44
|
+
exposure_margin: exposure_margin,
|
45
|
+
available_balance: available_balance,
|
46
|
+
variable_margin: variable_margin,
|
47
|
+
insufficient_balance: insufficient_balance,
|
48
|
+
brokerage: brokerage,
|
49
|
+
leverage: leverage
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
# Lightweight wrapper exposing market feed resources.
|
6
|
+
class MarketFeed < BaseModel
|
7
|
+
class << self
|
8
|
+
# Fetches last traded price snapshots.
|
9
|
+
#
|
10
|
+
# @param params [Hash]
|
11
|
+
# @return [Hash]
|
12
|
+
def ltp(params)
|
13
|
+
resource.ltp(params)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Fetches OHLC data for the requested instruments.
|
17
|
+
#
|
18
|
+
# @param params [Hash]
|
19
|
+
# @return [Hash]
|
20
|
+
def ohlc(params)
|
21
|
+
resource.ohlc(params)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches full quote depth and analytics.
|
25
|
+
#
|
26
|
+
# @param params [Hash]
|
27
|
+
# @return [Hash]
|
28
|
+
def quote(params)
|
29
|
+
resource.quote(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Shared market feed resource instance.
|
33
|
+
#
|
34
|
+
# @return [DhanHQ::Resources::MarketFeed]
|
35
|
+
def resource
|
36
|
+
@resource ||= DhanHQ::Resources::MarketFeed.new
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../contracts/option_chain_contract"
|
4
|
+
|
5
|
+
module DhanHQ
|
6
|
+
module Models
|
7
|
+
# Model for fetching and filtering option chain snapshots.
|
8
|
+
class OptionChain < BaseModel
|
9
|
+
attr_reader :underlying_scrip, :underlying_seg, :expiry, :last_price, :option_data
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Shared resource for option chain operations.
|
13
|
+
#
|
14
|
+
# @return [DhanHQ::Resources::OptionChain]
|
15
|
+
def resource
|
16
|
+
@resource ||= DhanHQ::Resources::OptionChain.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Fetch the entire option chain for an instrument
|
20
|
+
#
|
21
|
+
# @param params [Hash] The request parameters (snake_case format)
|
22
|
+
# @return [HashWithIndifferentAccess] The filtered option chain data
|
23
|
+
def fetch(params)
|
24
|
+
validate_params!(params, DhanHQ::Contracts::OptionChainContract)
|
25
|
+
|
26
|
+
response = resource.fetch(params)
|
27
|
+
return {}.with_indifferent_access unless response[:status] == "success"
|
28
|
+
|
29
|
+
filter_valid_strikes(response[:data]).with_indifferent_access
|
30
|
+
end
|
31
|
+
|
32
|
+
# Fetch the expiry list of an underlying security
|
33
|
+
#
|
34
|
+
# @param params [Hash] The request parameters (snake_case format)
|
35
|
+
# @return [Array<String>] The list of expiry dates
|
36
|
+
def fetch_expiry_list(params)
|
37
|
+
response = resource.expirylist(params)
|
38
|
+
response[:status] == "success" ? response[:data] : []
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# **Filters valid strikes where `ce` or `pe` has `last_price > 0` and keeps strike prices as-is**
|
44
|
+
#
|
45
|
+
# @param data [Hash] The API response data
|
46
|
+
# @return [Hash] The filtered option chain data with original strike price keys
|
47
|
+
def filter_valid_strikes(data)
|
48
|
+
return {} unless data.is_a?(Hash) && data.key?(:oc)
|
49
|
+
|
50
|
+
filtered_oc = data[:oc].each_with_object({}) do |(strike_price, strike_data), result|
|
51
|
+
ce_last_price = strike_data.dig("ce", "last_price").to_f
|
52
|
+
pe_last_price = strike_data.dig("pe", "last_price").to_f
|
53
|
+
|
54
|
+
# Only keep strikes where at least one of CE or PE has a valid last_price
|
55
|
+
result[strike_price] = strike_data if ce_last_price.positive? || pe_last_price.positive?
|
56
|
+
end
|
57
|
+
|
58
|
+
data.merge(oc: filtered_oc)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Validation contract for option chain
|
62
|
+
#
|
63
|
+
# @return [DhanHQ::Contracts::OptionChainContract]
|
64
|
+
def validation_contract
|
65
|
+
DhanHQ::Contracts::OptionChainContract.new
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Validation contract for option chain
|
72
|
+
#
|
73
|
+
# @return [DhanHQ::Contracts::OptionChainContract]
|
74
|
+
def validation_contract
|
75
|
+
DhanHQ::Contracts::OptionChainContract.new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../contracts/place_order_contract"
|
4
|
+
require_relative "../contracts/modify_order_contract"
|
5
|
+
|
6
|
+
module DhanHQ
|
7
|
+
# ActiveRecord-style models built on top of the REST resources.
|
8
|
+
module Models
|
9
|
+
# Representation of an order as returned by the REST APIs.
|
10
|
+
class Order < BaseModel
|
11
|
+
# Attributes eligible for modification requests.
|
12
|
+
MODIFIABLE_FIELDS = %i[
|
13
|
+
dhan_client_id
|
14
|
+
order_id
|
15
|
+
order_type
|
16
|
+
quantity
|
17
|
+
price
|
18
|
+
trigger_price
|
19
|
+
disclosed_quantity
|
20
|
+
validity
|
21
|
+
leg_name
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
attr_reader :order_id, :order_status
|
25
|
+
|
26
|
+
# Define attributes that are part of an order
|
27
|
+
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
28
|
+
:transaction_type, :exchange_segment, :product_type, :order_type,
|
29
|
+
:validity, :trading_symbol, :security_id, :quantity,
|
30
|
+
:disclosed_quantity, :price, :trigger_price, :after_market_order,
|
31
|
+
:bo_profit_value, :bo_stop_loss_value, :leg_name, :create_time,
|
32
|
+
:update_time, :exchange_time, :drv_expiry_date, :drv_option_type,
|
33
|
+
:drv_strike_price, :oms_error_code, :oms_error_description, :algo_id,
|
34
|
+
:remaining_quantity, :average_traded_price, :filled_qty
|
35
|
+
|
36
|
+
class << self
|
37
|
+
##
|
38
|
+
# Provides a **shared instance** of the `Orders` resource
|
39
|
+
#
|
40
|
+
# @return [DhanHQ::Resources::Orders]
|
41
|
+
def resource
|
42
|
+
@resource ||= DhanHQ::Resources::Orders.new
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Fetch all orders for the day.
|
47
|
+
#
|
48
|
+
# @return [Array<Order>]
|
49
|
+
def all
|
50
|
+
response = resource.all
|
51
|
+
return [] unless response.is_a?(Array)
|
52
|
+
|
53
|
+
response.map { |order| new(order, skip_validation: true) }
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Fetch a specific order by ID.
|
58
|
+
#
|
59
|
+
# @param order_id [String]
|
60
|
+
# @return [Order, nil]
|
61
|
+
def find(order_id)
|
62
|
+
response = resource.find(order_id)
|
63
|
+
return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
|
64
|
+
|
65
|
+
order_data = response.is_a?(Array) ? response.first : response
|
66
|
+
new(order_data, skip_validation: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Fetch a specific order by correlation ID.
|
71
|
+
#
|
72
|
+
# @param correlation_id [String]
|
73
|
+
# @return [Order, nil]
|
74
|
+
def find_by_correlation(correlation_id)
|
75
|
+
response = resource.by_correlation(correlation_id)
|
76
|
+
return nil unless response[:status] == "success"
|
77
|
+
|
78
|
+
new(response, skip_validation: true)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Place a new order
|
82
|
+
#
|
83
|
+
# @param params [Hash] Order parameters
|
84
|
+
# @return [Order]
|
85
|
+
def place(params)
|
86
|
+
normalized_params = snake_case(params)
|
87
|
+
validate_params!(normalized_params, DhanHQ::Contracts::PlaceOrderContract)
|
88
|
+
|
89
|
+
response = resource.create(camelize_keys(normalized_params))
|
90
|
+
return nil unless response.is_a?(Hash) && response["orderId"]
|
91
|
+
|
92
|
+
# Fetch the complete order details
|
93
|
+
find(response["orderId"])
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# AR-like create: new => valid? => save => resource.create
|
98
|
+
# But we can also define a class method if we want direct:
|
99
|
+
# Order.create(order_params)
|
100
|
+
#
|
101
|
+
# For the typical usage "Order.new(...).save", we rely on #save below.
|
102
|
+
def create(params)
|
103
|
+
order = new(params) # build it
|
104
|
+
return order unless order.valid? # run place order contract?
|
105
|
+
|
106
|
+
order.save # calls resource create or update
|
107
|
+
order
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Modify the order while preserving existing attributes
|
112
|
+
#
|
113
|
+
# @param new_params [Hash]
|
114
|
+
# @return [Order, nil]
|
115
|
+
def modify(new_params)
|
116
|
+
raise "Order ID is required to modify an order" unless id
|
117
|
+
|
118
|
+
base_payload = attributes.merge(new_params)
|
119
|
+
normalized_payload = snake_case(base_payload).merge(order_id: id)
|
120
|
+
filtered_payload = normalized_payload.each_with_object({}) do |(key, value), memo|
|
121
|
+
symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
122
|
+
memo[symbolized_key] = value if MODIFIABLE_FIELDS.include?(symbolized_key)
|
123
|
+
end
|
124
|
+
filtered_payload[:order_id] ||= id
|
125
|
+
filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id]
|
126
|
+
|
127
|
+
cleaned_payload = filtered_payload.compact
|
128
|
+
formatted_payload = camelize_keys(cleaned_payload)
|
129
|
+
validate_params!(formatted_payload, DhanHQ::Contracts::ModifyOrderContract)
|
130
|
+
|
131
|
+
response = self.class.resource.update(id, formatted_payload)
|
132
|
+
response = response.with_indifferent_access if response.respond_to?(:with_indifferent_access)
|
133
|
+
|
134
|
+
return DhanHQ::ErrorObject.new(response) unless success_response?(response)
|
135
|
+
|
136
|
+
@attributes.merge!(normalize_keys(response))
|
137
|
+
assign_attributes
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
# Cancel the order
|
142
|
+
#
|
143
|
+
# @return [Boolean]
|
144
|
+
def cancel
|
145
|
+
raise "Order ID is required to cancel an order" unless id
|
146
|
+
|
147
|
+
response = self.class.resource.cancel(id)
|
148
|
+
response["orderStatus"] == "CANCELLED"
|
149
|
+
end
|
150
|
+
|
151
|
+
# Fetch the latest details of the order
|
152
|
+
#
|
153
|
+
# @return [Order, nil]
|
154
|
+
def refresh
|
155
|
+
raise "Order ID is required to refresh an order" unless id
|
156
|
+
|
157
|
+
self.class.find(id)
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# This is how we figure out if it's an existing record or not:
|
162
|
+
def new_record?
|
163
|
+
order_id.nil? || order_id.to_s.empty?
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# The ID used for resource calls
|
168
|
+
def id
|
169
|
+
order_id
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Save: If new_record?, do resource.create
|
174
|
+
# else resource.update
|
175
|
+
def save
|
176
|
+
return false unless valid?
|
177
|
+
|
178
|
+
if new_record?
|
179
|
+
# PLACE ORDER
|
180
|
+
response = self.class.resource.create(to_request_params)
|
181
|
+
if success_response?(response) && response["orderId"]
|
182
|
+
@attributes.merge!(normalize_keys(response))
|
183
|
+
assign_attributes
|
184
|
+
true
|
185
|
+
else
|
186
|
+
# maybe store errors?
|
187
|
+
false
|
188
|
+
end
|
189
|
+
else
|
190
|
+
# MODIFY ORDER
|
191
|
+
response = self.class.resource.update(id, to_request_params)
|
192
|
+
if success_response?(response) && response["orderStatus"]
|
193
|
+
@attributes.merge!(normalize_keys(response))
|
194
|
+
assign_attributes
|
195
|
+
true
|
196
|
+
else
|
197
|
+
false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Cancel => calls resource.delete
|
204
|
+
def destroy
|
205
|
+
return false if new_record?
|
206
|
+
|
207
|
+
response = self.class.resource.delete(id)
|
208
|
+
if success_response?(response) && response["orderStatus"] == "CANCELLED"
|
209
|
+
@attributes[:order_status] = "CANCELLED"
|
210
|
+
true
|
211
|
+
else
|
212
|
+
false
|
213
|
+
end
|
214
|
+
end
|
215
|
+
alias delete destroy
|
216
|
+
|
217
|
+
##
|
218
|
+
# Slicing (optional)
|
219
|
+
# If you want an AR approach:
|
220
|
+
def slice_order(params)
|
221
|
+
raise "Order ID is required to slice an order" unless id
|
222
|
+
|
223
|
+
base_payload = params.merge(order_id: id)
|
224
|
+
formatted_payload = camelize_keys(base_payload)
|
225
|
+
|
226
|
+
validate_params!(formatted_payload, DhanHQ::Contracts::SliceOrderContract)
|
227
|
+
|
228
|
+
self.class.resource.slicing(formatted_payload)
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
# Because we have two separate contracts: place vs. modify
|
233
|
+
# We can do something like:
|
234
|
+
def validation_contract
|
235
|
+
new_record? ? DhanHQ::Contracts::PlaceOrderContract.new : DhanHQ::Contracts::ModifyOrderContract.new
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
# Model representing an intraday or carry-forward position snapshot.
|
6
|
+
class Position < BaseModel
|
7
|
+
# Base path used by the positions resource.
|
8
|
+
HTTP_PATH = "/v2/positions"
|
9
|
+
|
10
|
+
attributes :dhan_client_id, :trading_symbol, :security_id, :position_type, :exchange_segment,
|
11
|
+
:product_type, :buy_avg, :buy_qty, :cost_price, :sell_avg, :sell_qty,
|
12
|
+
:net_qty, :realized_profit, :unrealized_profit, :rbi_reference_rate, :multiplier,
|
13
|
+
:carry_forward_buy_qty, :carry_forward_sell_qty, :carry_forward_buy_value,
|
14
|
+
:carry_forward_sell_value, :day_buy_qty, :day_sell_qty, :day_buy_value,
|
15
|
+
:day_sell_value, :drv_expiry_date, :drv_option_type, :drv_strike_price,
|
16
|
+
:cross_currency
|
17
|
+
|
18
|
+
class << self
|
19
|
+
##
|
20
|
+
# Provides a **shared instance** of the `Positions` resource.
|
21
|
+
#
|
22
|
+
# @return [DhanHQ::Resources::Positions]
|
23
|
+
def resource
|
24
|
+
@resource ||= DhanHQ::Resources::Positions.new
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Fetch all positions for the day.
|
29
|
+
#
|
30
|
+
# @return [Array<Position>]
|
31
|
+
def all
|
32
|
+
response = resource.all
|
33
|
+
return [] unless response.is_a?(Array)
|
34
|
+
|
35
|
+
response.map do |position|
|
36
|
+
new(snake_case(position), skip_validation: true)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Filters the position list down to non-closed entries.
|
41
|
+
#
|
42
|
+
# @return [Array<Position>]
|
43
|
+
def active
|
44
|
+
all.reject { |position| position.position_type == "CLOSED" }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Convert an existing position (intraday <-> delivery)
|
48
|
+
# @param params [Hash] parameters as required by the API
|
49
|
+
# @return [Hash, DhanHQ::ErrorObject]
|
50
|
+
def convert(params)
|
51
|
+
formatted_params = camelize_keys(params)
|
52
|
+
validate_params!(formatted_params, DhanHQ::Contracts::PositionConversionContract)
|
53
|
+
|
54
|
+
response = resource.convert(formatted_params)
|
55
|
+
success_response?(response) ? response : DhanHQ::ErrorObject.new(response)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
##
|
6
|
+
# Ruby wrapper around the `/v2/profile` endpoint. Provides typed accessors
|
7
|
+
# and snake_case keys while leaving the underlying response untouched.
|
8
|
+
class Profile < BaseModel
|
9
|
+
# Base path for profile retrieval.
|
10
|
+
HTTP_PATH = "/v2/profile"
|
11
|
+
|
12
|
+
attributes :dhan_client_id, :token_validity, :active_segment, :ddpi,
|
13
|
+
:mtf, :data_plan, :data_validity
|
14
|
+
|
15
|
+
class << self
|
16
|
+
##
|
17
|
+
# Provides a shared instance of the profile resource.
|
18
|
+
#
|
19
|
+
# @return [DhanHQ::Resources::Profile]
|
20
|
+
def resource
|
21
|
+
@resource ||= DhanHQ::Resources::Profile.new
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Fetch the authenticated user's profile details.
|
26
|
+
#
|
27
|
+
# @return [DhanHQ::Models::Profile, nil]
|
28
|
+
def fetch
|
29
|
+
response = resource.fetch
|
30
|
+
return nil unless response.is_a?(Hash)
|
31
|
+
|
32
|
+
new(response, skip_validation: true)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Profile responses are informational and not validated locally.
|
37
|
+
#
|
38
|
+
# @return [nil]
|
39
|
+
def validation_contract
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module Models
|
5
|
+
# Model wrapping multi-leg super order payloads.
|
6
|
+
class SuperOrder < BaseModel
|
7
|
+
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
8
|
+
:transaction_type, :exchange_segment, :product_type, :order_type,
|
9
|
+
:validity, :trading_symbol, :security_id, :quantity,
|
10
|
+
:remaining_quantity, :ltp, :price, :after_market_order,
|
11
|
+
:leg_name, :exchange_order_id, :create_time, :update_time,
|
12
|
+
:exchange_time, :oms_error_description, :average_traded_price,
|
13
|
+
:filled_qty, :leg_details, :target_price, :stop_loss_price,
|
14
|
+
:trailing_jump
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Shared resource instance used for API calls.
|
18
|
+
#
|
19
|
+
# @return [DhanHQ::Resources::SuperOrders]
|
20
|
+
def resource
|
21
|
+
@resource ||= DhanHQ::Resources::SuperOrders.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Fetches all configured super orders.
|
25
|
+
#
|
26
|
+
# @return [Array<SuperOrder>]
|
27
|
+
def all
|
28
|
+
response = resource.all
|
29
|
+
return [] unless response.is_a?(Array)
|
30
|
+
|
31
|
+
response.map { |o| new(o, skip_validation: true) }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates a new super order with the provided legs.
|
35
|
+
#
|
36
|
+
# @param params [Hash]
|
37
|
+
# @return [SuperOrder, nil]
|
38
|
+
def create(params)
|
39
|
+
response = resource.create(params)
|
40
|
+
return nil unless response.is_a?(Hash) && response["orderId"]
|
41
|
+
|
42
|
+
new(order_id: response["orderId"], order_status: response["orderStatus"], skip_validation: true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Updates the order legs for an existing super order.
|
47
|
+
#
|
48
|
+
# @param new_params [Hash]
|
49
|
+
# @return [Boolean]
|
50
|
+
def modify(new_params)
|
51
|
+
raise "Order ID is required to modify a super order" unless id
|
52
|
+
|
53
|
+
response = self.class.resource.update(id, new_params)
|
54
|
+
response["orderId"] == id
|
55
|
+
end
|
56
|
+
|
57
|
+
# Cancels a specific leg (or the entry leg by default).
|
58
|
+
#
|
59
|
+
# @param leg_name [String]
|
60
|
+
# @return [Boolean]
|
61
|
+
def cancel(leg_name = "ENTRY_LEG")
|
62
|
+
raise "Order ID is required to cancel a super order" unless id
|
63
|
+
|
64
|
+
response = self.class.resource.cancel(id, leg_name)
|
65
|
+
response["orderStatus"] == "CANCELLED"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|