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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -1
  3. data/CHANGELOG.md +103 -7
  4. data/GUIDE.md +57 -39
  5. data/README.md +198 -755
  6. data/docs/API_DOCS_GAPS.md +128 -0
  7. data/docs/API_VERIFICATION.md +10 -11
  8. data/{README1.md → docs/ARCHIVE_README.md} +16 -16
  9. data/docs/AUTHENTICATION.md +72 -10
  10. data/docs/CONFIGURATION.md +109 -0
  11. data/docs/CONSTANTS_REFERENCE.md +477 -0
  12. data/docs/DATA_API_PARAMETERS.md +7 -7
  13. data/docs/{rails_websocket_integration.md → RAILS_WEBSOCKET_INTEGRATION.md} +10 -10
  14. data/docs/{standalone_ruby_websocket_integration.md → STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md} +32 -32
  15. data/docs/SUPER_ORDERS.md +284 -0
  16. data/docs/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
  17. data/docs/TESTING_GUIDE.md +84 -82
  18. data/docs/TROUBLESHOOTING.md +117 -0
  19. data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
  20. data/docs/WEBSOCKET_PROTOCOL.md +154 -0
  21. data/lib/DhanHQ/constants.rb +456 -151
  22. data/lib/DhanHQ/contracts/alert_order_contract.rb +37 -10
  23. data/lib/DhanHQ/contracts/base_contract.rb +22 -0
  24. data/lib/DhanHQ/contracts/edis_contract.rb +25 -0
  25. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +27 -4
  26. data/lib/DhanHQ/contracts/modify_order_contract.rb +65 -12
  27. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +23 -0
  28. data/lib/DhanHQ/contracts/order_contract.rb +171 -39
  29. data/lib/DhanHQ/contracts/place_order_contract.rb +14 -141
  30. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +20 -0
  31. data/lib/DhanHQ/contracts/position_conversion_contract.rb +15 -3
  32. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -1
  33. data/lib/DhanHQ/contracts/user_ip_contract.rb +14 -0
  34. data/lib/DhanHQ/core/base_model.rb +13 -4
  35. data/lib/DhanHQ/helpers/response_helper.rb +2 -2
  36. data/lib/DhanHQ/helpers/validation_helper.rb +1 -1
  37. data/lib/DhanHQ/models/alert_order.rb +29 -11
  38. data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
  39. data/lib/DhanHQ/models/edis.rb +101 -0
  40. data/lib/DhanHQ/models/expired_options_data.rb +6 -12
  41. data/lib/DhanHQ/models/forever_order.rb +8 -5
  42. data/lib/DhanHQ/models/historical_data.rb +0 -8
  43. data/lib/DhanHQ/models/instrument.rb +1 -7
  44. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  45. data/lib/DhanHQ/models/kill_switch.rb +23 -11
  46. data/lib/DhanHQ/models/margin.rb +51 -2
  47. data/lib/DhanHQ/models/order.rb +107 -126
  48. data/lib/DhanHQ/models/order_update.rb +7 -13
  49. data/lib/DhanHQ/models/pnl_exit.rb +122 -0
  50. data/lib/DhanHQ/models/position.rb +23 -1
  51. data/lib/DhanHQ/models/postback.rb +114 -0
  52. data/lib/DhanHQ/models/profile.rb +0 -10
  53. data/lib/DhanHQ/models/super_order.rb +13 -3
  54. data/lib/DhanHQ/models/trade.rb +11 -23
  55. data/lib/DhanHQ/resources/ip_setup.rb +16 -5
  56. data/lib/DhanHQ/resources/kill_switch.rb +17 -7
  57. data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
  58. data/lib/DhanHQ/resources/orders.rb +41 -41
  59. data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
  60. data/lib/DhanHQ/resources/positions.rb +8 -0
  61. data/lib/DhanHQ/version.rb +1 -1
  62. data/lib/DhanHQ/ws/cmd_bus.rb +1 -1
  63. data/lib/DhanHQ/ws/orders/client.rb +6 -6
  64. data/lib/DhanHQ/ws/singleton_lock.rb +2 -1
  65. data/lib/dhanhq/analysis/options_buying_advisor.rb +2 -2
  66. data/lib/rubocop/cop/dhanhq/use_constants.rb +171 -0
  67. metadata +29 -24
  68. data/TODO-1.md +0 -14
  69. data/TODO.md +0 -127
  70. data/app/services/live/order_update_guard_support.rb +0 -75
  71. data/app/services/live/order_update_hub.rb +0 -76
  72. data/app/services/live/order_update_persistence_support.rb +0 -68
  73. data/docs/PR_2.2.0.md +0 -48
  74. data/examples/comprehensive_websocket_examples.rb +0 -148
  75. data/examples/instrument_finder_test.rb +0 -195
  76. data/examples/live_order_updates.rb +0 -118
  77. data/examples/market_depth_example.rb +0 -144
  78. data/examples/market_feed_example.rb +0 -81
  79. data/examples/order_update_example.rb +0 -105
  80. data/examples/trading_fields_example.rb +0 -215
  81. /data/docs/{live_order_updates.md → LIVE_ORDER_UPDATES.md} +0 -0
  82. /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 (exchange_segment, security_id,
6
- # condition, trigger_price, transaction_type, quantity; optional price, order_type).
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(:exchange_segment).filled(:string)
10
- required(:security_id).filled(:string)
11
- required(:condition).filled(:string)
12
- required(:trigger_price).filled(:float)
13
- required(:transaction_type).filled(:string, included_in?: %w[BUY SELL])
14
- required(:quantity).filled(:integer, gt?: 0)
15
- optional(:price).maybe(:float)
16
- optional(:order_type).maybe(:string)
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?: %w[NSE_EQ NSE_FNO BSE_EQ])
10
- required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
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?: %w[CNC INTRADAY MARGIN MTF CO BO])
12
+ required(:productType).filled(:string, included_in?: DhanHQ::Constants::ProductType::ALL)
13
13
  required(:securityId).filled(:string)
14
- required(:price).filled(:float, gt?: 0)
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
- # Validation contract for order modification requests
6
- class ModifyOrderContract < Dry::Validation::Contract
7
+ # Contract for validating order modification requests
8
+ class ModifyOrderContract < OrderContract
7
9
  params do
8
- required(:dhanClientId).filled(:string)
9
- required(:orderId).filled(:string)
10
- optional(:orderType).maybe(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
11
- optional(:quantity).maybe(:integer)
12
- optional(:price).maybe(:float)
13
- optional(:triggerPrice).maybe(:float)
14
- optional(:disclosedQuantity).maybe(:integer)
15
- optional(:validity).maybe(:string, included_in?: %w[DAY IOC])
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
- rule(:quantity) do
19
- key.failure("must be provided if modifying quantity") if value.nil? && values[:price].nil?
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
- # Shared contract used to validate place and modify order payloads.
8
+ # Base contract for validating order placements and rules
9
9
  class OrderContract < BaseContract
10
- # Allowed transaction directions supported by DhanHQ order APIs.
11
- TRANSACTION_TYPES = %w[BUY SELL].freeze
12
- # Supported exchange segments for order placement requests.
13
- EXCHANGE_SEGMENTS = %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ MCX_COMM BSE_CURRENCY BSE_FNO].freeze
14
- # Permitted product types for order placement.
15
- PRODUCT_TYPES = %w[CNC INTRADAY MARGIN CO BO].freeze
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) # For modifications
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
- # Conditional validation rules
47
- rule(:price) do
48
- key.failure("must be present for LIMIT orders") if values[:order_type] == "LIMIT" && !value
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]) && !value
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
- rule(:amo_time) do
58
- key.failure("must be present for after market orders") if values[:after_market_order] == true && !value
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
- rule(:bo_profit_value, :bo_stop_loss_value) do
62
- if values[:product_type] == "BO" && (!values[:bo_profit_value] || !values[:bo_stop_loss_value])
63
- key.failure("both profit and stop loss values required for BO orders")
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(:disclosed_quantity) do
68
- key.failure("cannot exceed 30% of total quantity") if value && value > (values[:quantity] * 0.3)
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
- # Modification specific rules (when extending)
72
- rule(:leg_name) do
73
- if values[:product_type] == "BO" && !%w[ENTRY_LEG TARGET_LEG STOP_LOSS_LEG].include?(value)
74
- key.failure("invalid leg name for BO order")
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