DhanHQ 2.8.0 → 3.0.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/.rubocop_todo.yml +7 -0
- data/CHANGELOG.md +19 -0
- data/README.md +16 -5
- data/docs/RAILS_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +1 -1
- data/docs/TECHNICAL_ANALYSIS.md +1 -1
- data/exe/DhanHQ +1 -1
- data/lib/DhanHQ/agent/order_preview.rb +50 -0
- data/lib/DhanHQ/agent/policy.rb +51 -0
- data/lib/DhanHQ/agent/tool_registry.rb +250 -0
- data/lib/DhanHQ/agent.rb +12 -0
- data/lib/DhanHQ/ai/context_builder.rb +145 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +114 -0
- data/lib/DhanHQ/ai.rb +27 -0
- data/lib/DhanHQ/auth.rb +0 -1
- data/lib/DhanHQ/client.rb +1 -3
- data/lib/DhanHQ/constants.rb +2 -0
- data/lib/DhanHQ/contracts/iceberg_order_contract.rb +83 -0
- data/lib/DhanHQ/contracts/twap_order_contract.rb +106 -0
- data/lib/DhanHQ/core/auth_api.rb +0 -1
- data/lib/DhanHQ/errors.rb +4 -0
- data/lib/DhanHQ/events/base.rb +203 -0
- data/lib/DhanHQ/events/bus.rb +158 -0
- data/lib/DhanHQ/events.rb +40 -0
- data/lib/DhanHQ/indicators.rb +283 -0
- data/lib/DhanHQ/market_data/market_snapshot.rb +97 -0
- data/lib/DhanHQ/market_data/ohlc_series.rb +169 -0
- data/lib/DhanHQ/market_data/option_snapshot.rb +223 -0
- data/lib/DhanHQ/market_data.rb +25 -0
- data/lib/DhanHQ/mcp/server.rb +72 -0
- data/lib/DhanHQ/mcp.rb +10 -0
- data/lib/DhanHQ/models/funds.rb +12 -0
- data/lib/DhanHQ/models/holding.rb +42 -0
- data/lib/DhanHQ/models/iceberg_order.rb +139 -0
- data/lib/DhanHQ/models/instrument.rb +36 -0
- data/lib/DhanHQ/models/order.rb +95 -0
- data/lib/DhanHQ/models/position.rb +66 -0
- data/lib/DhanHQ/models/search_result.rb +12 -0
- data/lib/DhanHQ/models/trade.rb +13 -0
- data/lib/DhanHQ/models/twap_order.rb +136 -0
- data/lib/DhanHQ/option_analytics/black_scholes.rb +194 -0
- data/lib/DhanHQ/option_analytics/max_pain.rb +119 -0
- data/lib/DhanHQ/option_analytics.rb +36 -0
- data/lib/DhanHQ/resources/iceberg_orders.rb +61 -0
- data/lib/DhanHQ/resources/twap_orders.rb +61 -0
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +17 -0
- data/lib/DhanHQ/risk/checks/market_hours.rb +37 -0
- data/lib/DhanHQ/risk/checks/options.rb +46 -0
- data/lib/DhanHQ/risk/checks/order_type.rb +20 -0
- data/lib/DhanHQ/risk/checks/product_support.rb +34 -0
- data/lib/DhanHQ/risk/checks/quantity.rb +32 -0
- data/lib/DhanHQ/risk/checks/trading_permission.rb +16 -0
- data/lib/DhanHQ/risk/pipeline.rb +65 -0
- data/lib/DhanHQ/risk.rb +250 -0
- data/lib/DhanHQ/skills/base.rb +132 -0
- data/lib/DhanHQ/skills/builtin/buy_atm_call.rb +87 -0
- data/lib/DhanHQ/skills/builtin/iron_condor.rb +93 -0
- data/lib/DhanHQ/skills/builtin/square_off_all.rb +45 -0
- data/lib/DhanHQ/skills/builtin/square_off_position.rb +48 -0
- data/lib/DhanHQ/skills/builtin/strangle.rb +93 -0
- data/lib/DhanHQ/skills/registry.rb +101 -0
- data/lib/DhanHQ/skills/workflow.rb +66 -0
- data/lib/DhanHQ/skills.rb +29 -0
- data/lib/DhanHQ/strategy/base.rb +189 -0
- data/lib/DhanHQ/strategy.rb +40 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/decoder.rb +57 -19
- data/lib/DhanHQ.rb +3 -0
- data/lib/dhan_hq/agent.rb +3 -0
- data/lib/dhan_hq/analysis.rb +9 -0
- data/lib/dhan_hq/mcp.rb +3 -0
- data/lib/dhan_hq/ta.rb +5 -0
- data/lib/dhan_hq.rb +27 -4
- data/lib/ta/technical_analysis.rb +3 -1
- data/skills/dhanhq-ruby/SKILL.md +74 -0
- data/skills/dhanhq-ruby/references/market_data.md +3 -0
- data/skills/dhanhq-ruby/references/orders.md +7 -0
- metadata +61 -20
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../contracts/iceberg_order_contract"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Models
|
|
7
|
+
##
|
|
8
|
+
# Model for creating and managing Iceberg Orders.
|
|
9
|
+
#
|
|
10
|
+
# Iceberg orders allow you to place a large order while revealing only a small
|
|
11
|
+
# "visible" portion (disclosed quantity) to the market at a time. The remaining
|
|
12
|
+
# quantity is hidden and filled as prior legs execute, reducing market impact
|
|
13
|
+
# and information leakage.
|
|
14
|
+
#
|
|
15
|
+
# @note **Static IP Whitelisting**: Iceberg order creation, modification, and
|
|
16
|
+
# cancellation APIs require Static IP whitelisting.
|
|
17
|
+
#
|
|
18
|
+
# @example Create an iceberg order
|
|
19
|
+
# order = DhanHQ::Models::IcebergOrder.create(
|
|
20
|
+
# dhan_client_id: "1000000003",
|
|
21
|
+
# transaction_type: "BUY",
|
|
22
|
+
# exchange_segment: "NSE_EQ",
|
|
23
|
+
# product_type: "INTRADAY",
|
|
24
|
+
# order_type: "LIMIT",
|
|
25
|
+
# validity: "DAY",
|
|
26
|
+
# security_id: "11536",
|
|
27
|
+
# quantity: 1000,
|
|
28
|
+
# price: 2450.50,
|
|
29
|
+
# iceberg_qty: 100,
|
|
30
|
+
# disclosed_quantity: 100
|
|
31
|
+
# )
|
|
32
|
+
# puts "Iceberg Order ID: #{order.order_id} - #{order.order_status}"
|
|
33
|
+
#
|
|
34
|
+
# @example Modify an iceberg order (change visible leg size)
|
|
35
|
+
# order = DhanHQ::Models::IcebergOrder.find(order_id)
|
|
36
|
+
# order.modify(
|
|
37
|
+
# dhan_client_id: "1000000003",
|
|
38
|
+
# order_id: order_id,
|
|
39
|
+
# price: 2440.0,
|
|
40
|
+
# iceberg_qty: 150
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# @example Cancel an iceberg order
|
|
44
|
+
# order = DhanHQ::Models::IcebergOrder.find(order_id)
|
|
45
|
+
# order.cancel
|
|
46
|
+
#
|
|
47
|
+
# @example List all iceberg orders for the day
|
|
48
|
+
# orders = DhanHQ::Models::IcebergOrder.all
|
|
49
|
+
#
|
|
50
|
+
class IcebergOrder < BaseModel
|
|
51
|
+
include Concerns::ApiResponseHandler
|
|
52
|
+
|
|
53
|
+
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
54
|
+
:transaction_type, :exchange_segment, :product_type, :order_type,
|
|
55
|
+
:validity, :trading_symbol, :security_id, :quantity,
|
|
56
|
+
:remaining_quantity, :price, :trigger_price, :after_market_order,
|
|
57
|
+
:iceberg_qty, :disclosed_quantity, :leg_name,
|
|
58
|
+
:create_time, :update_time, :exchange_time, :drv_expiry_date,
|
|
59
|
+
:drv_option_type, :drv_strike_price,
|
|
60
|
+
:oms_error_code, :oms_error_description, :average_traded_price, :filled_qty
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
# @return [DhanHQ::Resources::IcebergOrders]
|
|
64
|
+
def resource
|
|
65
|
+
@resource ||= DhanHQ::Resources::IcebergOrders.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Retrieves all iceberg orders for the day.
|
|
69
|
+
#
|
|
70
|
+
# @return [Array<IcebergOrder>]
|
|
71
|
+
def all
|
|
72
|
+
response = resource.all
|
|
73
|
+
return [] unless response.is_a?(Array)
|
|
74
|
+
|
|
75
|
+
response.map { |o| new(o, skip_validation: true) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Retrieves a specific iceberg order by ID.
|
|
79
|
+
#
|
|
80
|
+
# @param order_id [String]
|
|
81
|
+
# @return [IcebergOrder, nil]
|
|
82
|
+
def find(order_id)
|
|
83
|
+
response = resource.find(order_id)
|
|
84
|
+
return nil unless response.is_a?(Hash) && response.any?
|
|
85
|
+
|
|
86
|
+
new(response, skip_validation: true)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Creates a new iceberg order.
|
|
90
|
+
#
|
|
91
|
+
# @param params [Hash{Symbol => String, Integer, Float}]
|
|
92
|
+
# @return [IcebergOrder, nil]
|
|
93
|
+
def create(params)
|
|
94
|
+
normalized = snake_case(params)
|
|
95
|
+
config = DhanHQ.configuration
|
|
96
|
+
normalized[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
97
|
+
validate_params!(normalized, DhanHQ::Contracts::IcebergOrderCreateContract)
|
|
98
|
+
formatted = camelize_keys(normalized)
|
|
99
|
+
response = resource.create(formatted)
|
|
100
|
+
return nil unless response.is_a?(Hash) && response["orderId"]
|
|
101
|
+
|
|
102
|
+
new(order_id: response["orderId"], order_status: response["orderStatus"], skip_validation: true)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Modifies an existing iceberg order.
|
|
107
|
+
#
|
|
108
|
+
# @param new_params [Hash{Symbol => String, Integer, Float}]
|
|
109
|
+
# @return [IcebergOrder, nil]
|
|
110
|
+
def modify(new_params)
|
|
111
|
+
raise "Order ID is required to modify an iceberg order" unless order_id
|
|
112
|
+
|
|
113
|
+
DhanHQ.logger&.info("[DhanHQ::Models::IcebergOrder] Modifying order #{order_id}")
|
|
114
|
+
full_params = snake_case(new_params)
|
|
115
|
+
config = DhanHQ.configuration
|
|
116
|
+
full_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
117
|
+
full_params[:order_id] = order_id
|
|
118
|
+
validate_params!(full_params, DhanHQ::Contracts::IcebergOrderModifyContract)
|
|
119
|
+
formatted = camelize_keys(full_params)
|
|
120
|
+
response = self.class.resource.update(order_id, formatted)
|
|
121
|
+
success = handle_api_response(response, success_key: "orderId",
|
|
122
|
+
context: "[DhanHQ::Models::IcebergOrder] Modification")
|
|
123
|
+
return self.class.find(order_id) if success
|
|
124
|
+
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Cancels the iceberg order.
|
|
129
|
+
#
|
|
130
|
+
# @return [Boolean] true when cancelled successfully
|
|
131
|
+
def cancel
|
|
132
|
+
raise "Order ID is required to cancel an iceberg order" unless order_id
|
|
133
|
+
|
|
134
|
+
response = self.class.resource.cancel(order_id)
|
|
135
|
+
response.is_a?(Hash) && response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../contracts/instrument_list_contract"
|
|
4
4
|
require_relative "instrument_helpers"
|
|
5
|
+
require_relative "search_result"
|
|
5
6
|
|
|
6
7
|
module DhanHQ
|
|
7
8
|
module Models
|
|
@@ -114,6 +115,41 @@ module DhanHQ
|
|
|
114
115
|
nil
|
|
115
116
|
end
|
|
116
117
|
|
|
118
|
+
# Search instruments across segments and return agent-friendly search results.
|
|
119
|
+
# @param query [String] symbol/company/index text
|
|
120
|
+
# @param options [Hash] :segments, :limit, :exact_match, :case_sensitive
|
|
121
|
+
# @return [Array<DhanHQ::Models::SearchResult>]
|
|
122
|
+
def search(query, options = {})
|
|
123
|
+
raise ArgumentError, "query is required" if query.to_s.strip.empty?
|
|
124
|
+
|
|
125
|
+
limit = Integer(options.fetch(:limit, 20))
|
|
126
|
+
segments = options[:segments] || %w[NSE_EQ BSE_EQ IDX_I NSE_FNO BSE_FNO NSE_CURRENCY BSE_CURRENCY MCX_COMM]
|
|
127
|
+
exact_match = options.fetch(:exact_match, false)
|
|
128
|
+
case_sensitive = options.fetch(:case_sensitive, false)
|
|
129
|
+
needle = case_sensitive ? query.to_s : query.to_s.upcase
|
|
130
|
+
|
|
131
|
+
results = []
|
|
132
|
+
segments.each do |segment|
|
|
133
|
+
by_segment(segment).each do |instrument|
|
|
134
|
+
haystacks = [
|
|
135
|
+
instrument.symbol_name,
|
|
136
|
+
instrument.display_name,
|
|
137
|
+
instrument.underlying_symbol,
|
|
138
|
+
instrument.isin
|
|
139
|
+
].compact
|
|
140
|
+
matched = haystacks.any? do |value|
|
|
141
|
+
comparable = case_sensitive ? value.to_s : value.to_s.upcase
|
|
142
|
+
exact_match ? comparable == needle : comparable.include?(needle)
|
|
143
|
+
end
|
|
144
|
+
next unless matched
|
|
145
|
+
|
|
146
|
+
results << DhanHQ::Models::SearchResult.new(instrument.attributes, skip_validation: true)
|
|
147
|
+
return results if results.length >= limit
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
results
|
|
151
|
+
end
|
|
152
|
+
|
|
117
153
|
def normalize_csv_row(row)
|
|
118
154
|
# Extract exchange and segment from CSV
|
|
119
155
|
exchange_id = row["EXCH_ID"] || row["EXCHANGE"]
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -73,6 +73,101 @@ module DhanHQ
|
|
|
73
73
|
:drv_strike_price, :oms_error_code, :oms_error_description, :algo_id,
|
|
74
74
|
:remaining_quantity, :average_traded_price, :filled_qty
|
|
75
75
|
|
|
76
|
+
# Returns a concise prompt-friendly summary of the order.
|
|
77
|
+
# Useful for AI agents to understand order state without raw attributes.
|
|
78
|
+
def to_prompt
|
|
79
|
+
parts = [
|
|
80
|
+
"#{transaction_type} #{quantity}x #{trading_symbol || security_id}",
|
|
81
|
+
"on #{exchange_segment}",
|
|
82
|
+
"as #{order_type}/#{product_type}",
|
|
83
|
+
"status=#{order_status}"
|
|
84
|
+
]
|
|
85
|
+
parts << "price=#{price}" if price&.to_f&.positive?
|
|
86
|
+
parts << "trigger=#{trigger_price}" if trigger_price&.to_f&.positive?
|
|
87
|
+
parts << "filled=#{filled_qty}" if filled_qty&.positive?
|
|
88
|
+
parts << "remaining=#{remaining_quantity}" if remaining_quantity&.positive?
|
|
89
|
+
parts << "id=#{order_id}" if order_id
|
|
90
|
+
parts << "correlation=#{correlation_id}" if correlation_id
|
|
91
|
+
parts.join(", ")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns true if the order has been fully filled.
|
|
95
|
+
def filled?
|
|
96
|
+
order_status == DhanHQ::Constants::OrderStatus::TRADED
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns true if the order is pending execution.
|
|
100
|
+
def pending?
|
|
101
|
+
order_status == DhanHQ::Constants::OrderStatus::PENDING
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns true if the order was rejected by the exchange.
|
|
105
|
+
def rejected?
|
|
106
|
+
order_status == DhanHQ::Constants::OrderStatus::REJECTED
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Returns true if the order was cancelled.
|
|
110
|
+
def cancelled?
|
|
111
|
+
order_status == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Returns true if the order is in transit to the exchange.
|
|
115
|
+
def transit?
|
|
116
|
+
order_status == DhanHQ::Constants::OrderStatus::TRANSIT
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns true if the order is partially filled.
|
|
120
|
+
def partially_filled?
|
|
121
|
+
filled_qty.to_i.positive? && remaining_quantity.to_i.positive?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Returns the fill percentage (0.0 to 100.0).
|
|
125
|
+
def fill_percentage
|
|
126
|
+
return 0.0 if quantity.to_i.zero?
|
|
127
|
+
|
|
128
|
+
(filled_qty.to_i.to_f / quantity.to_i * 100).round(2)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns true if this is a buy order.
|
|
132
|
+
def buy?
|
|
133
|
+
transaction_type == DhanHQ::Constants::TransactionType::BUY
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns true if this is a sell order.
|
|
137
|
+
def sell?
|
|
138
|
+
transaction_type == DhanHQ::Constants::TransactionType::SELL
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns true if this is an intraday order.
|
|
142
|
+
def intraday?
|
|
143
|
+
product_type == DhanHQ::Constants::ProductType::INTRADAY
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns true if this is a delivery (CNC) order.
|
|
147
|
+
def delivery?
|
|
148
|
+
product_type == DhanHQ::Constants::ProductType::CNC
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns true if this is an MTF order.
|
|
152
|
+
def mtf?
|
|
153
|
+
product_type == DhanHQ::Constants::ProductType::MTF
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns true if this is a market order.
|
|
157
|
+
def market_order?
|
|
158
|
+
order_type == DhanHQ::Constants::OrderType::MARKET
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Returns true if this is a limit order.
|
|
162
|
+
def limit_order?
|
|
163
|
+
order_type == DhanHQ::Constants::OrderType::LIMIT
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns true if this is a stop-loss order.
|
|
167
|
+
def stop_loss_order?
|
|
168
|
+
order_type&.start_with?(DhanHQ::Constants::OrderType::STOP_LOSS)
|
|
169
|
+
end
|
|
170
|
+
|
|
76
171
|
class << self
|
|
77
172
|
##
|
|
78
173
|
# Provides a shared instance of the Orders resource.
|
|
@@ -44,6 +44,72 @@ module DhanHQ
|
|
|
44
44
|
:day_sell_value, :drv_expiry_date, :drv_option_type, :drv_strike_price,
|
|
45
45
|
:cross_currency
|
|
46
46
|
|
|
47
|
+
# Returns a concise prompt-friendly summary of the position.
|
|
48
|
+
def to_prompt
|
|
49
|
+
parts = [
|
|
50
|
+
"#{position_type} #{net_qty}x #{trading_symbol || security_id}",
|
|
51
|
+
"on #{exchange_segment}/#{product_type}"
|
|
52
|
+
]
|
|
53
|
+
parts << "buy_avg=#{buy_avg}" if buy_avg&.positive?
|
|
54
|
+
parts << "sell_avg=#{sell_avg}" if sell_avg&.positive?
|
|
55
|
+
parts << "realized_pnl=#{realized_profit}" if realized_profit
|
|
56
|
+
parts << "unrealized_pnl=#{unrealized_profit}" if unrealized_profit
|
|
57
|
+
parts << "expiry=#{drv_expiry_date}" if drv_expiry_date
|
|
58
|
+
parts << "strike=#{drv_strike_price}" if drv_strike_price&.positive?
|
|
59
|
+
parts << "type=#{drv_option_type}" if drv_option_type
|
|
60
|
+
parts.join(", ")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns true if this is a long position.
|
|
64
|
+
def long?
|
|
65
|
+
position_type == DhanHQ::Constants::PositionType::LONG
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns true if this is a short position.
|
|
69
|
+
def short?
|
|
70
|
+
position_type == DhanHQ::Constants::PositionType::SHORT
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns true if this position is closed.
|
|
74
|
+
def closed?
|
|
75
|
+
position_type == DhanHQ::Constants::OrderStatus::CLOSED || net_qty.to_i.zero?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns true if this position has an open quantity.
|
|
79
|
+
def open?
|
|
80
|
+
!closed? && net_qty.to_i != 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns true if this position is profitable (unrealized).
|
|
84
|
+
def profitable?
|
|
85
|
+
unrealized_profit.to_f.positive?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns true if this position is in loss (unrealized).
|
|
89
|
+
def in_loss?
|
|
90
|
+
unrealized_profit.to_f.negative?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the total unrealized P&L including realized.
|
|
94
|
+
def total_pnl
|
|
95
|
+
realized_profit.to_f + unrealized_profit.to_f
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns true if this is an F&O position.
|
|
99
|
+
def derivatives?
|
|
100
|
+
drv_expiry_date.present? || drv_strike_price.to_f.positive?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns true if this is an options position.
|
|
104
|
+
def options?
|
|
105
|
+
derivatives? && drv_option_type.present?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns true if this is a futures position.
|
|
109
|
+
def futures?
|
|
110
|
+
derivatives? && drv_option_type.nil?
|
|
111
|
+
end
|
|
112
|
+
|
|
47
113
|
class << self
|
|
48
114
|
##
|
|
49
115
|
# Provides a shared instance of the Positions resource.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
# Lightweight result returned by Instrument.search for agent-friendly security resolution.
|
|
6
|
+
class SearchResult < BaseModel
|
|
7
|
+
attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument,
|
|
8
|
+
:instrument_type, :lot_size, :tick_size, :expiry_date, :strike_price,
|
|
9
|
+
:option_type, :underlying_symbol, :isin
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/DhanHQ/models/trade.rb
CHANGED
|
@@ -44,6 +44,19 @@ module DhanHQ
|
|
|
44
44
|
:create_time, :update_time, :exchange_time, :drv_expiry_date,
|
|
45
45
|
:drv_option_type, :drv_strike_price
|
|
46
46
|
|
|
47
|
+
# Returns a concise prompt-friendly summary of the trade.
|
|
48
|
+
def to_prompt
|
|
49
|
+
parts = [
|
|
50
|
+
"#{transaction_type} #{traded_quantity}x #{trading_symbol || security_id}",
|
|
51
|
+
"@ ₹#{traded_price}",
|
|
52
|
+
"on #{exchange_segment}/#{product_type}"
|
|
53
|
+
]
|
|
54
|
+
parts << "order=#{order_id}" if order_id
|
|
55
|
+
parts << "charges=₹#{total_charges}" if respond_to?(:total_charges) && total_charges
|
|
56
|
+
parts << "time=#{create_time}" if create_time
|
|
57
|
+
parts.join(", ")
|
|
58
|
+
end
|
|
59
|
+
|
|
47
60
|
class << self
|
|
48
61
|
##
|
|
49
62
|
# Provides a shared instance of the Trades resource for current day tradebook APIs.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../contracts/twap_order_contract"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Models
|
|
7
|
+
##
|
|
8
|
+
# Model for creating and managing TWAP Orders.
|
|
9
|
+
#
|
|
10
|
+
# TWAP (Time-Weighted Average Price) orders split a large order into smaller
|
|
11
|
+
# slices distributed evenly across a defined time window. This minimizes market
|
|
12
|
+
# impact and achieves execution closer to the period's average price.
|
|
13
|
+
#
|
|
14
|
+
# @note **Static IP Whitelisting**: TWAP order creation, modification, and
|
|
15
|
+
# cancellation APIs require Static IP whitelisting.
|
|
16
|
+
#
|
|
17
|
+
# @example Create a TWAP order
|
|
18
|
+
# order = DhanHQ::Models::TwapOrder.create(
|
|
19
|
+
# dhan_client_id: "1000000003",
|
|
20
|
+
# transaction_type: "SELL",
|
|
21
|
+
# exchange_segment: "NSE_EQ",
|
|
22
|
+
# product_type: "INTRADAY",
|
|
23
|
+
# order_type: "MARKET",
|
|
24
|
+
# validity: "DAY",
|
|
25
|
+
# security_id: "11536",
|
|
26
|
+
# quantity: 500,
|
|
27
|
+
# price: 0, # MARKET order
|
|
28
|
+
# slice_interval: 300, # 5 minutes
|
|
29
|
+
# start_time: "09:30:00",
|
|
30
|
+
# end_time: "15:00:00"
|
|
31
|
+
# )
|
|
32
|
+
# puts "TWAP Order ID: #{order.order_id} - #{order.order_status}"
|
|
33
|
+
#
|
|
34
|
+
# @example Modify a TWAP order (extend window)
|
|
35
|
+
# order = DhanHQ::Models::TwapOrder.find(order_id)
|
|
36
|
+
# order.modify(
|
|
37
|
+
# dhan_client_id: "1000000003",
|
|
38
|
+
# order_id: order_id,
|
|
39
|
+
# end_time: "15:30:00",
|
|
40
|
+
# slice_interval: 600
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
43
|
+
# @example Cancel a TWAP order
|
|
44
|
+
# order = DhanHQ::Models::TwapOrder.find(order_id)
|
|
45
|
+
# order.cancel
|
|
46
|
+
#
|
|
47
|
+
class TwapOrder < BaseModel
|
|
48
|
+
include Concerns::ApiResponseHandler
|
|
49
|
+
|
|
50
|
+
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
51
|
+
:transaction_type, :exchange_segment, :product_type, :order_type,
|
|
52
|
+
:validity, :trading_symbol, :security_id, :quantity,
|
|
53
|
+
:remaining_quantity, :price, :trigger_price, :after_market_order,
|
|
54
|
+
:slice_interval, :start_time, :end_time, :leg_name,
|
|
55
|
+
:create_time, :update_time, :exchange_time, :drv_expiry_date,
|
|
56
|
+
:drv_option_type, :drv_strike_price,
|
|
57
|
+
:oms_error_code, :oms_error_description, :average_traded_price, :filled_qty
|
|
58
|
+
|
|
59
|
+
class << self
|
|
60
|
+
# @return [DhanHQ::Resources::TwapOrders]
|
|
61
|
+
def resource
|
|
62
|
+
@resource ||= DhanHQ::Resources::TwapOrders.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Retrieves all TWAP orders for the day.
|
|
66
|
+
#
|
|
67
|
+
# @return [Array<TwapOrder>]
|
|
68
|
+
def all
|
|
69
|
+
response = resource.all
|
|
70
|
+
return [] unless response.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
response.map { |o| new(o, skip_validation: true) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Retrieves a specific TWAP order by ID.
|
|
76
|
+
#
|
|
77
|
+
# @param order_id [String]
|
|
78
|
+
# @return [TwapOrder, nil]
|
|
79
|
+
def find(order_id)
|
|
80
|
+
response = resource.find(order_id)
|
|
81
|
+
return nil unless response.is_a?(Hash) && response.any?
|
|
82
|
+
|
|
83
|
+
new(response, skip_validation: true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Creates a new TWAP order.
|
|
87
|
+
#
|
|
88
|
+
# @param params [Hash{Symbol => String, Integer, Float}]
|
|
89
|
+
# @return [TwapOrder, nil]
|
|
90
|
+
def create(params)
|
|
91
|
+
normalized = snake_case(params)
|
|
92
|
+
config = DhanHQ.configuration
|
|
93
|
+
normalized[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
94
|
+
validate_params!(normalized, DhanHQ::Contracts::TwapOrderCreateContract)
|
|
95
|
+
formatted = camelize_keys(normalized)
|
|
96
|
+
response = resource.create(formatted)
|
|
97
|
+
return nil unless response.is_a?(Hash) && response["orderId"]
|
|
98
|
+
|
|
99
|
+
new(order_id: response["orderId"], order_status: response["orderStatus"], skip_validation: true)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Modifies an existing TWAP order.
|
|
104
|
+
#
|
|
105
|
+
# @param new_params [Hash{Symbol => String, Integer, Float}]
|
|
106
|
+
# @return [TwapOrder, nil]
|
|
107
|
+
def modify(new_params)
|
|
108
|
+
raise "Order ID is required to modify a TWAP order" unless order_id
|
|
109
|
+
|
|
110
|
+
DhanHQ.logger&.info("[DhanHQ::Models::TwapOrder] Modifying order #{order_id}")
|
|
111
|
+
full_params = snake_case(new_params)
|
|
112
|
+
config = DhanHQ.configuration
|
|
113
|
+
full_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
114
|
+
full_params[:order_id] = order_id
|
|
115
|
+
validate_params!(full_params, DhanHQ::Contracts::TwapOrderModifyContract)
|
|
116
|
+
formatted = camelize_keys(full_params)
|
|
117
|
+
response = self.class.resource.update(order_id, formatted)
|
|
118
|
+
success = handle_api_response(response, success_key: "orderId",
|
|
119
|
+
context: "[DhanHQ::Models::TwapOrder] Modification")
|
|
120
|
+
return self.class.find(order_id) if success
|
|
121
|
+
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Cancels the TWAP order.
|
|
126
|
+
#
|
|
127
|
+
# @return [Boolean] true when cancelled successfully
|
|
128
|
+
def cancel
|
|
129
|
+
raise "Order ID is required to cancel a TWAP order" unless order_id
|
|
130
|
+
|
|
131
|
+
response = self.class.resource.cancel(order_id)
|
|
132
|
+
response.is_a?(Hash) && response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|