DhanHQ 2.4.0 → 2.6.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/.rubocop.yml +9 -1
- data/CHANGELOG.md +103 -7
- data/GUIDE.md +57 -39
- data/README.md +198 -755
- data/docs/API_DOCS_GAPS.md +128 -0
- data/docs/API_VERIFICATION.md +10 -11
- data/{README1.md → docs/ARCHIVE_README.md} +16 -16
- data/docs/AUTHENTICATION.md +72 -10
- data/docs/CONFIGURATION.md +109 -0
- data/docs/CONSTANTS_REFERENCE.md +477 -0
- data/docs/DATA_API_PARAMETERS.md +7 -7
- data/docs/{rails_websocket_integration.md → RAILS_WEBSOCKET_INTEGRATION.md} +10 -10
- data/docs/{standalone_ruby_websocket_integration.md → STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md} +32 -32
- data/docs/SUPER_ORDERS.md +284 -0
- data/docs/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
- data/docs/TESTING_GUIDE.md +84 -82
- data/docs/TROUBLESHOOTING.md +117 -0
- data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
- data/docs/WEBSOCKET_PROTOCOL.md +154 -0
- data/lib/DhanHQ/constants.rb +456 -151
- data/lib/DhanHQ/contracts/alert_order_contract.rb +37 -10
- data/lib/DhanHQ/contracts/base_contract.rb +22 -0
- data/lib/DhanHQ/contracts/edis_contract.rb +25 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +27 -4
- data/lib/DhanHQ/contracts/modify_order_contract.rb +65 -12
- data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +23 -0
- data/lib/DhanHQ/contracts/order_contract.rb +171 -39
- data/lib/DhanHQ/contracts/place_order_contract.rb +14 -141
- data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +20 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +15 -3
- data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -1
- data/lib/DhanHQ/contracts/user_ip_contract.rb +14 -0
- data/lib/DhanHQ/core/base_model.rb +13 -4
- data/lib/DhanHQ/helpers/response_helper.rb +2 -2
- data/lib/DhanHQ/helpers/validation_helper.rb +1 -1
- data/lib/DhanHQ/models/alert_order.rb +29 -11
- data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
- data/lib/DhanHQ/models/edis.rb +101 -0
- data/lib/DhanHQ/models/expired_options_data.rb +6 -12
- data/lib/DhanHQ/models/forever_order.rb +8 -5
- data/lib/DhanHQ/models/historical_data.rb +0 -8
- data/lib/DhanHQ/models/instrument.rb +1 -7
- data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
- data/lib/DhanHQ/models/kill_switch.rb +23 -11
- data/lib/DhanHQ/models/margin.rb +51 -2
- data/lib/DhanHQ/models/order.rb +107 -126
- data/lib/DhanHQ/models/order_update.rb +7 -13
- data/lib/DhanHQ/models/pnl_exit.rb +122 -0
- data/lib/DhanHQ/models/position.rb +23 -1
- data/lib/DhanHQ/models/postback.rb +114 -0
- data/lib/DhanHQ/models/profile.rb +0 -10
- data/lib/DhanHQ/models/super_order.rb +13 -3
- data/lib/DhanHQ/models/trade.rb +11 -23
- data/lib/DhanHQ/resources/ip_setup.rb +16 -5
- data/lib/DhanHQ/resources/kill_switch.rb +17 -7
- data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
- data/lib/DhanHQ/resources/orders.rb +41 -41
- data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
- data/lib/DhanHQ/resources/positions.rb +8 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/cmd_bus.rb +1 -1
- data/lib/DhanHQ/ws/orders/client.rb +6 -6
- data/lib/DhanHQ/ws/singleton_lock.rb +2 -1
- data/lib/dhanhq/analysis/options_buying_advisor.rb +2 -2
- data/lib/rubocop/cop/dhanhq/use_constants.rb +171 -0
- metadata +29 -24
- data/TODO-1.md +0 -14
- data/TODO.md +0 -127
- data/app/services/live/order_update_guard_support.rb +0 -75
- data/app/services/live/order_update_hub.rb +0 -76
- data/app/services/live/order_update_persistence_support.rb +0 -68
- data/docs/PR_2.2.0.md +0 -48
- data/examples/comprehensive_websocket_examples.rb +0 -148
- data/examples/instrument_finder_test.rb +0 -195
- data/examples/live_order_updates.rb +0 -118
- data/examples/market_depth_example.rb +0 -144
- data/examples/market_feed_example.rb +0 -81
- data/examples/order_update_example.rb +0 -105
- data/examples/trading_fields_example.rb +0 -215
- /data/docs/{live_order_updates.md → LIVE_ORDER_UPDATES.md} +0 -0
- /data/docs/{rails_integration.md → RAILS_INTEGRATION.md} +0 -0
|
@@ -2,18 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates alert order payloads for create/update
|
|
6
|
-
#
|
|
5
|
+
# Validates alert order payloads for create/update per dhanhq.co/docs/v2/conditional-trigger/
|
|
6
|
+
# Condition requires exchange_segment, exp_date, frequency; time_frame required for TECHNICAL_* comparison types.
|
|
7
7
|
class AlertOrderContract < BaseContract
|
|
8
8
|
params do
|
|
9
|
-
required(:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
required(:condition).hash do
|
|
10
|
+
required(:security_id).filled(:string, max_size?: 20)
|
|
11
|
+
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
12
|
+
required(:comparison_type).filled(:string, included_in?: COMPARISON_TYPES)
|
|
13
|
+
optional(:indicator_name).maybe(:string)
|
|
14
|
+
optional(:time_frame).maybe(:string)
|
|
15
|
+
required(:operator).filled(:string, included_in?: OPERATORS)
|
|
16
|
+
optional(:comparing_value).maybe(:float)
|
|
17
|
+
optional(:comparing_indicator_name).maybe(:string)
|
|
18
|
+
required(:exp_date).filled(:string)
|
|
19
|
+
required(:frequency).filled(:string)
|
|
20
|
+
end
|
|
21
|
+
required(:orders).array(:hash) do
|
|
22
|
+
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
23
|
+
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
24
|
+
required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
|
|
25
|
+
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
26
|
+
required(:security_id).filled(:string, max_size?: 20)
|
|
27
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
28
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
29
|
+
optional(:price).maybe(:float)
|
|
30
|
+
optional(:trigger_price).maybe(:float)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rule(condition: :indicator_name) do
|
|
35
|
+
if values[:condition] && values[:condition][:comparison_type].to_s.start_with?("TECHNICAL") && !value
|
|
36
|
+
key(condition: :indicator_name).failure("is required for technical comparisons")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
rule(condition: :time_frame) do
|
|
41
|
+
if values[:condition] && values[:condition][:comparison_type].to_s.start_with?("TECHNICAL") && !value
|
|
42
|
+
key(condition: :time_frame).failure("is required for technical comparisons")
|
|
43
|
+
end
|
|
17
44
|
end
|
|
18
45
|
end
|
|
19
46
|
end
|
|
@@ -10,6 +10,28 @@ module DhanHQ
|
|
|
10
10
|
class BaseContract < Dry::Validation::Contract
|
|
11
11
|
# Include constants to make them accessible in all derived contracts
|
|
12
12
|
include DhanHQ::Constants
|
|
13
|
+
|
|
14
|
+
# Optional instrument metadata used by subcontracts for lot/tick size validation
|
|
15
|
+
option :instrument_meta, optional: true
|
|
16
|
+
|
|
17
|
+
register_macro(:lot_size_multiple) do
|
|
18
|
+
meta = _contract.instrument_meta
|
|
19
|
+
next unless meta && meta[:lot_size]
|
|
20
|
+
|
|
21
|
+
ls = meta[:lot_size]
|
|
22
|
+
key.failure("must be a multiple of lot size (#{ls})") if value && (value % ls != 0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
register_macro(:tick_size_multiple) do
|
|
26
|
+
meta = _contract.instrument_meta
|
|
27
|
+
next unless meta && meta[:tick_size]
|
|
28
|
+
|
|
29
|
+
ts = meta[:tick_size]
|
|
30
|
+
if value
|
|
31
|
+
quotient = value.to_f / ts
|
|
32
|
+
key.failure("must be a multiple of tick size (#{ts})") if (quotient - quotient.round).abs > 1e-6
|
|
33
|
+
end
|
|
34
|
+
end
|
|
13
35
|
end
|
|
14
36
|
end
|
|
15
37
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates requests for generating EDIS forms.
|
|
6
|
+
class EdisFormContract < BaseContract
|
|
7
|
+
params do
|
|
8
|
+
required(:isin).filled(:string)
|
|
9
|
+
required(:qty).filled(:integer, gt?: 0)
|
|
10
|
+
required(:exchange).filled(:string, included_in?: %w[NSE BSE MCX ALL])
|
|
11
|
+
required(:segment).filled(:string, included_in?: %w[EQ COMM FNO])
|
|
12
|
+
required(:bulk).filled(:bool)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Validates requests for generating bulk EDIS forms.
|
|
17
|
+
class EdisBulkFormContract < BaseContract
|
|
18
|
+
params do
|
|
19
|
+
required(:isin).array(:string)
|
|
20
|
+
required(:exchange).filled(:string, included_in?: %w[NSE BSE MCX ALL])
|
|
21
|
+
required(:segment).filled(:string, included_in?: %w[EQ COMM FNO])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -6,14 +6,37 @@ module DhanHQ
|
|
|
6
6
|
class MarginCalculatorContract < Dry::Validation::Contract
|
|
7
7
|
params do
|
|
8
8
|
required(:dhanClientId).filled(:string)
|
|
9
|
-
required(:exchangeSegment).filled(:string, included_in?:
|
|
10
|
-
required(:transactionType).filled(:string, included_in?:
|
|
9
|
+
required(:exchangeSegment).filled(:string, included_in?: DhanHQ::Constants::ExchangeSegment::ALL)
|
|
10
|
+
required(:transactionType).filled(:string, included_in?: DhanHQ::Constants::TransactionType::ALL)
|
|
11
11
|
required(:quantity).filled(:integer, gt?: 0)
|
|
12
|
-
required(:productType).filled(:string, included_in?:
|
|
12
|
+
required(:productType).filled(:string, included_in?: DhanHQ::Constants::ProductType::ALL)
|
|
13
13
|
required(:securityId).filled(:string)
|
|
14
|
-
|
|
14
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
15
15
|
optional(:triggerPrice).maybe(:float)
|
|
16
16
|
end
|
|
17
|
+
rule(:price) do
|
|
18
|
+
if values[:price]
|
|
19
|
+
if values[:price] <= 0
|
|
20
|
+
key(:price).failure("must be greater than 0")
|
|
21
|
+
elsif values[:price].is_a?(Float) && (values[:price].nan? || values[:price].infinite?)
|
|
22
|
+
key(:price).failure("must be a finite number")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule(:triggerPrice) do
|
|
28
|
+
key(:triggerPrice).failure("must be a finite number") if values[:triggerPrice].is_a?(Float) && (values[:triggerPrice].nan? || values[:triggerPrice].infinite?)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Segment-Based Product Restrictions for margin calculations
|
|
32
|
+
rule(:productType, :exchangeSegment) do
|
|
33
|
+
case values[:productType]
|
|
34
|
+
when DhanHQ::Constants::ProductType::CNC, DhanHQ::Constants::ProductType::MTF
|
|
35
|
+
key(:productType).failure("is only allowed for Equity segments (NSE_EQ, BSE_EQ)") unless /_EQ$/.match?(values[:exchangeSegment])
|
|
36
|
+
when DhanHQ::Constants::ProductType::MARGIN
|
|
37
|
+
key(:productType).failure("is not allowed for Equity Cash segments; use CNC or INTRADAY") if /_EQ$/.match?(values[:exchangeSegment])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
17
40
|
end
|
|
18
41
|
end
|
|
19
42
|
end
|
|
@@ -1,22 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "order_contract"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Contracts
|
|
5
|
-
#
|
|
6
|
-
class ModifyOrderContract <
|
|
7
|
+
# Contract for validating order modification requests
|
|
8
|
+
class ModifyOrderContract < OrderContract
|
|
7
9
|
params do
|
|
8
|
-
required(:
|
|
9
|
-
|
|
10
|
-
optional(:
|
|
11
|
-
optional(:
|
|
12
|
-
optional(:
|
|
13
|
-
optional(:
|
|
14
|
-
optional(:
|
|
15
|
-
optional(:
|
|
10
|
+
required(:order_id).filled(:string)
|
|
11
|
+
|
|
12
|
+
optional(:transaction_type).maybe(:string, included_in?: TRANSACTION_TYPES)
|
|
13
|
+
optional(:exchange_segment).maybe(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
14
|
+
optional(:product_type).maybe(:string, included_in?: PRODUCT_TYPES)
|
|
15
|
+
optional(:order_type).maybe(:string, included_in?: ORDER_TYPES)
|
|
16
|
+
optional(:validity).maybe(:string, included_in?: VALIDITY_TYPES)
|
|
17
|
+
optional(:security_id).maybe(:string, max_size?: 20)
|
|
18
|
+
optional(:quantity).maybe(:integer, gt?: 0)
|
|
19
|
+
|
|
20
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
21
|
+
# Allow 0 for non–stop-loss orders (API often returns triggerPrice: 0); rule below enforces > 0 for SL types.
|
|
22
|
+
optional(:trigger_price).maybe(:float, gteq?: 0)
|
|
23
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
24
|
+
|
|
25
|
+
optional(:bo_profit_value).maybe(:float, gt?: 0)
|
|
26
|
+
optional(:bo_stop_loss_value).maybe(:float, gt?: 0)
|
|
27
|
+
optional(:leg_name).maybe(:string)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# --------------------------------------------------
|
|
31
|
+
# MUST MODIFY AT LEAST ONE EXECUTION FIELD
|
|
32
|
+
# --------------------------------------------------
|
|
33
|
+
|
|
34
|
+
rule do
|
|
35
|
+
modifiable_fields = %i[
|
|
36
|
+
quantity price trigger_price disclosed_quantity
|
|
37
|
+
bo_profit_value bo_stop_loss_value
|
|
38
|
+
validity order_type
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
changed = modifiable_fields.any? { |field| values.key?(field) && !values[field].nil? }
|
|
42
|
+
|
|
43
|
+
base.failure("at least one modifiable field must be provided") unless changed
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------
|
|
47
|
+
# MARKET ORDER RESTRICTION
|
|
48
|
+
# --------------------------------------------------
|
|
49
|
+
|
|
50
|
+
rule(:order_type, :price) do
|
|
51
|
+
key(:price).failure("cannot modify price for MARKET orders") if values[:order_type] == DhanHQ::Constants::OrderType::MARKET && values[:price]
|
|
16
52
|
end
|
|
17
53
|
|
|
18
|
-
|
|
19
|
-
|
|
54
|
+
# --------------------------------------------------
|
|
55
|
+
# TRIGGER PRICE FOR STOP-LOSS ONLY
|
|
56
|
+
# --------------------------------------------------
|
|
57
|
+
|
|
58
|
+
rule(:order_type, :trigger_price) do
|
|
59
|
+
if %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type]) && (values[:trigger_price].nil? || values[:trigger_price].to_f <= 0)
|
|
60
|
+
key(:trigger_price).failure("must be present and greater than zero for STOP_LOSS orders")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --------------------------------------------------
|
|
65
|
+
# BO LEG VALIDATION
|
|
66
|
+
# --------------------------------------------------
|
|
67
|
+
|
|
68
|
+
rule(:leg_name, :product_type) do
|
|
69
|
+
if values[:product_type] == DhanHQ::Constants::ProductType::BO
|
|
70
|
+
allowed = %w[ENTRY_LEG TARGET_LEG STOP_LOSS_LEG]
|
|
71
|
+
key(:leg_name).failure("invalid leg_name for BO order") unless values[:leg_name] && allowed.include?(values[:leg_name])
|
|
72
|
+
end
|
|
20
73
|
end
|
|
21
74
|
end
|
|
22
75
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates requests for multi-scrip margin calculations.
|
|
6
|
+
class MultiScripMarginCalcRequestContract < BaseContract
|
|
7
|
+
params do
|
|
8
|
+
optional(:dhanClientId).maybe(:string)
|
|
9
|
+
optional(:includePosition).maybe(:bool)
|
|
10
|
+
optional(:includeOrder).maybe(:bool)
|
|
11
|
+
required(:scripList).array(:hash) do
|
|
12
|
+
required(:exchangeSegment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
13
|
+
required(:transactionType).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
14
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
15
|
+
required(:productType).filled(:string, included_in?: PRODUCT_TYPES)
|
|
16
|
+
required(:securityId).filled(:string)
|
|
17
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
18
|
+
optional(:triggerPrice).maybe(:float, gt?: 0)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# lib/dhan_hq/contracts/order_contract.rb
|
|
4
3
|
require "dry-validation"
|
|
4
|
+
require_relative "base_contract"
|
|
5
5
|
|
|
6
6
|
module DhanHQ
|
|
7
7
|
module Contracts
|
|
8
|
-
#
|
|
8
|
+
# Base contract for validating order placements and rules
|
|
9
9
|
class OrderContract < BaseContract
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Supported order execution types.
|
|
17
|
-
ORDER_TYPES = %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET].freeze
|
|
18
|
-
# Validity window options for orders.
|
|
19
|
-
VALIDITY_TYPES = %w[DAY IOC].freeze
|
|
20
|
-
# After-market execution windows.
|
|
21
|
-
AMO_TIMES = %w[PRE_OPEN OPEN OPEN_30 OPEN_60].freeze
|
|
10
|
+
TRANSACTION_TYPES = DhanHQ::Constants::TransactionType::ALL
|
|
11
|
+
EXCHANGE_SEGMENTS = DhanHQ::Constants::ExchangeSegment::ALL
|
|
12
|
+
PRODUCT_TYPES = DhanHQ::Constants::ProductType::ALL
|
|
13
|
+
ORDER_TYPES = DhanHQ::Constants::OrderType::ALL
|
|
14
|
+
VALIDITY_TYPES = DhanHQ::Constants::Validity::ALL
|
|
15
|
+
AMO_TIMES = DhanHQ::Constants::AmoTime::ALL
|
|
22
16
|
|
|
23
17
|
params do
|
|
24
|
-
# Common required fields
|
|
25
|
-
required(:dhan_client_id).filled(:string)
|
|
26
18
|
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
27
19
|
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
28
20
|
required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
|
|
@@ -31,47 +23,187 @@ module DhanHQ
|
|
|
31
23
|
required(:security_id).filled(:string)
|
|
32
24
|
required(:quantity).filled(:integer, gt?: 0)
|
|
33
25
|
|
|
34
|
-
# Optional fields
|
|
35
26
|
optional(:correlation_id).maybe(:string)
|
|
36
27
|
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
37
|
-
optional(:price).maybe(:float)
|
|
38
|
-
optional(:trigger_price).maybe(:float)
|
|
28
|
+
optional(:price).maybe(:float, gt?: 0)
|
|
29
|
+
optional(:trigger_price).maybe(:float, gt?: 0)
|
|
39
30
|
optional(:after_market_order).maybe(:bool)
|
|
40
31
|
optional(:amo_time).maybe(:string, included_in?: AMO_TIMES)
|
|
41
|
-
optional(:bo_profit_value).maybe(:float)
|
|
42
|
-
optional(:bo_stop_loss_value).maybe(:float)
|
|
43
|
-
optional(:leg_name).maybe(:string)
|
|
32
|
+
optional(:bo_profit_value).maybe(:float, gt?: 0)
|
|
33
|
+
optional(:bo_stop_loss_value).maybe(:float, gt?: 0)
|
|
34
|
+
optional(:leg_name).maybe(:string)
|
|
44
35
|
end
|
|
45
36
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
# --------------------------------------------------
|
|
38
|
+
# ORDER TYPE VALIDATION
|
|
39
|
+
# --------------------------------------------------
|
|
40
|
+
|
|
41
|
+
rule(:order_type, :price) do
|
|
42
|
+
if values[:order_type] == DhanHQ::Constants::OrderType::LIMIT && !values[:price]
|
|
43
|
+
key(:price).failure("must be present for LIMIT orders")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if values[:order_type] == DhanHQ::Constants::OrderType::MARKET && values[:price]
|
|
47
|
+
key(:price).failure("must not be provided for MARKET orders")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if values[:price].is_a?(Float) && (values[:price].nan? || values[:price].infinite?)
|
|
51
|
+
key(:price).failure("must be a finite number")
|
|
52
|
+
end
|
|
49
53
|
end
|
|
50
54
|
|
|
51
|
-
rule(:trigger_price) do
|
|
52
|
-
if %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type]) && !
|
|
53
|
-
key.failure("must be present for STOP_LOSS orders")
|
|
55
|
+
rule(:order_type, :trigger_price) do
|
|
56
|
+
if %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type]) && !values[:trigger_price]
|
|
57
|
+
key(:trigger_price).failure("must be present for STOP_LOSS orders")
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
# --------------------------------------------------
|
|
62
|
+
# STOP LOSS PRICE RELATIONSHIP
|
|
63
|
+
# --------------------------------------------------
|
|
64
|
+
|
|
65
|
+
rule(:order_type, :transaction_type, :price, :trigger_price) do
|
|
66
|
+
next unless %w[STOP_LOSS STOP_LOSS_MARKET].include?(values[:order_type])
|
|
67
|
+
next unless values[:price] && values[:trigger_price]
|
|
68
|
+
|
|
69
|
+
if values[:transaction_type] == DhanHQ::Constants::TransactionType::BUY
|
|
70
|
+
if values[:trigger_price] < values[:price]
|
|
71
|
+
key(:trigger_price).failure("must be >= price for BUY stop-loss")
|
|
72
|
+
end
|
|
73
|
+
elsif values[:transaction_type] == DhanHQ::Constants::TransactionType::SELL
|
|
74
|
+
if values[:trigger_price] > values[:price]
|
|
75
|
+
key(:trigger_price).failure("must be <= price for SELL stop-loss")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
59
78
|
end
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
# --------------------------------------------------
|
|
81
|
+
# BRACKET ORDER LOGIC
|
|
82
|
+
# --------------------------------------------------
|
|
83
|
+
|
|
84
|
+
rule(:product_type, :bo_profit_value, :bo_stop_loss_value) do
|
|
85
|
+
if (values[:product_type] == DhanHQ::Constants::ProductType::BO) && (!values[:bo_profit_value] || !values[:bo_stop_loss_value])
|
|
86
|
+
key.failure("both bo_profit_value and bo_stop_loss_value required for BO orders")
|
|
64
87
|
end
|
|
65
88
|
end
|
|
66
89
|
|
|
67
|
-
rule(:
|
|
68
|
-
|
|
90
|
+
rule(:product_type, :transaction_type, :price, :bo_profit_value, :bo_stop_loss_value) do
|
|
91
|
+
next unless values[:product_type] == DhanHQ::Constants::ProductType::BO
|
|
92
|
+
next unless values[:price] && values[:bo_profit_value] && values[:bo_stop_loss_value]
|
|
93
|
+
|
|
94
|
+
if values[:transaction_type] == DhanHQ::Constants::TransactionType::BUY
|
|
95
|
+
if values[:bo_stop_loss_value] >= values[:price]
|
|
96
|
+
key(:bo_stop_loss_value).failure("must be less than entry price for BUY BO")
|
|
97
|
+
end
|
|
98
|
+
if values[:bo_profit_value] <= values[:price]
|
|
99
|
+
key(:bo_profit_value).failure("must be greater than entry price for BUY BO")
|
|
100
|
+
end
|
|
101
|
+
elsif values[:transaction_type] == DhanHQ::Constants::TransactionType::SELL
|
|
102
|
+
if values[:bo_stop_loss_value] <= values[:price]
|
|
103
|
+
key(:bo_stop_loss_value).failure("must be greater than entry price for SELL BO")
|
|
104
|
+
end
|
|
105
|
+
if values[:bo_profit_value] >= values[:price]
|
|
106
|
+
key(:bo_profit_value).failure("must be less than entry price for SELL BO")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
69
109
|
end
|
|
70
110
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
# --------------------------------------------------
|
|
112
|
+
# DISCLOSED QUANTITY
|
|
113
|
+
# --------------------------------------------------
|
|
114
|
+
|
|
115
|
+
rule(:disclosed_quantity, :quantity) do
|
|
116
|
+
next unless values[:disclosed_quantity]
|
|
117
|
+
|
|
118
|
+
if values[:disclosed_quantity] > (values[:quantity] * 0.3)
|
|
119
|
+
key(:disclosed_quantity).failure("cannot exceed 30% of total quantity")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# --------------------------------------------------
|
|
124
|
+
# AMO VALIDATION
|
|
125
|
+
# --------------------------------------------------
|
|
126
|
+
|
|
127
|
+
rule(:after_market_order, :amo_time) do
|
|
128
|
+
if values[:after_market_order] == true && !values[:amo_time]
|
|
129
|
+
key(:amo_time).failure("must be present when after_market_order is true")
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------
|
|
134
|
+
# LOT SIZE ENFORCEMENT
|
|
135
|
+
# --------------------------------------------------
|
|
136
|
+
|
|
137
|
+
rule(:quantity) do
|
|
138
|
+
next unless instrument_meta
|
|
139
|
+
next unless instrument_meta[:lot_size]
|
|
140
|
+
next unless instrument_meta[:lot_size].positive?
|
|
141
|
+
|
|
142
|
+
lot = instrument_meta[:lot_size]
|
|
143
|
+
|
|
144
|
+
if value % lot != 0
|
|
145
|
+
key.failure("must be multiple of lot size #{lot}")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------
|
|
150
|
+
# TICK SIZE ENFORCEMENT
|
|
151
|
+
# --------------------------------------------------
|
|
152
|
+
|
|
153
|
+
rule(:price) do
|
|
154
|
+
next unless instrument_meta
|
|
155
|
+
next unless instrument_meta[:tick_size]
|
|
156
|
+
next unless value
|
|
157
|
+
|
|
158
|
+
tick = instrument_meta[:tick_size]
|
|
159
|
+
|
|
160
|
+
remainder = ((value.to_f / tick) % 1).round(10)
|
|
161
|
+
|
|
162
|
+
if remainder != 0
|
|
163
|
+
key.failure("must align with tick size #{tick}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
rule(:trigger_price) do
|
|
168
|
+
next unless instrument_meta
|
|
169
|
+
next unless instrument_meta[:tick_size]
|
|
170
|
+
next unless value
|
|
171
|
+
|
|
172
|
+
tick = instrument_meta[:tick_size]
|
|
173
|
+
|
|
174
|
+
remainder = ((value.to_f / tick) % 1).round(10)
|
|
175
|
+
|
|
176
|
+
if remainder != 0
|
|
177
|
+
key.failure("must align with tick size #{tick}")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# --------------------------------------------------
|
|
182
|
+
# SEGMENT RESTRICTIONS
|
|
183
|
+
# --------------------------------------------------
|
|
184
|
+
|
|
185
|
+
rule(:exchange_segment, :product_type) do
|
|
186
|
+
next unless values[:exchange_segment] && values[:product_type]
|
|
187
|
+
|
|
188
|
+
segment = values[:exchange_segment]
|
|
189
|
+
product = values[:product_type]
|
|
190
|
+
|
|
191
|
+
if product == DhanHQ::Constants::ProductType::CNC && !%w[NSE_EQ BSE_EQ].include?(segment)
|
|
192
|
+
key(:product_type).failure("is only allowed for Equity segments")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if product == DhanHQ::Constants::ProductType::MARGIN && %w[NSE_EQ BSE_EQ].include?(segment)
|
|
196
|
+
key(:product_type).failure("is not allowed for Equity Cash segments")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# BO not allowed in some segments
|
|
200
|
+
if %w[NSE_CURRENCY BSE_CURRENCY].include?(segment) && product == DhanHQ::Constants::ProductType::BO
|
|
201
|
+
key(:product_type).failure("BO not allowed for currency segment")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# CO restrictions example
|
|
205
|
+
if segment == DhanHQ::Constants::ExchangeSegment::NSE_EQ && product == DhanHQ::Constants::ProductType::CO
|
|
206
|
+
key(:product_type).failure("CO not supported in NSE_EQ")
|
|
75
207
|
end
|
|
76
208
|
end
|
|
77
209
|
end
|