ib-ruby 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/HISTORY +4 -0
  2. data/README.md +25 -17
  3. data/VERSION +1 -1
  4. data/bin/account_info +1 -1
  5. data/bin/cancel_orders +2 -1
  6. data/bin/contract_details +3 -2
  7. data/bin/depth_of_market +1 -1
  8. data/bin/historic_data +1 -1
  9. data/bin/historic_data_cli +57 -104
  10. data/bin/list_orders +4 -3
  11. data/bin/market_data +1 -1
  12. data/bin/option_data +1 -1
  13. data/bin/place_combo_order +63 -0
  14. data/bin/place_order +2 -4
  15. data/bin/template +1 -1
  16. data/bin/{generic_data.rb → tick_data} +3 -1
  17. data/bin/time_and_sales +1 -1
  18. data/lib/ib-ruby.rb +1 -0
  19. data/lib/ib-ruby/connection.rb +68 -68
  20. data/lib/ib-ruby/errors.rb +28 -0
  21. data/lib/ib-ruby/extensions.rb +7 -0
  22. data/lib/ib-ruby/messages.rb +1 -0
  23. data/lib/ib-ruby/messages/abstract_message.rb +16 -11
  24. data/lib/ib-ruby/messages/incoming.rb +125 -329
  25. data/lib/ib-ruby/messages/incoming/open_order.rb +193 -0
  26. data/lib/ib-ruby/messages/incoming/ticks.rb +131 -0
  27. data/lib/ib-ruby/messages/outgoing.rb +44 -45
  28. data/lib/ib-ruby/models/combo_leg.rb +16 -1
  29. data/lib/ib-ruby/models/contract.rb +18 -10
  30. data/lib/ib-ruby/models/contract/bag.rb +1 -7
  31. data/lib/ib-ruby/models/execution.rb +2 -1
  32. data/lib/ib-ruby/models/model.rb +1 -1
  33. data/lib/ib-ruby/models/order.rb +116 -56
  34. data/lib/ib-ruby/socket.rb +24 -3
  35. data/spec/account_helper.rb +2 -1
  36. data/spec/ib-ruby/messages/outgoing_spec.rb +1 -1
  37. data/spec/ib-ruby/models/combo_leg_spec.rb +0 -1
  38. data/spec/integration/account_info_spec.rb +2 -2
  39. data/spec/integration/contract_info_spec.rb +4 -4
  40. data/spec/integration/depth_data_spec.rb +3 -3
  41. data/spec/integration/historic_data_spec.rb +1 -1
  42. data/spec/integration/market_data_spec.rb +4 -4
  43. data/spec/integration/option_data_spec.rb +1 -1
  44. data/spec/integration/orders/combo_spec.rb +51 -0
  45. data/spec/integration/orders/execution_spec.rb +15 -8
  46. data/spec/integration/orders/placement_spec.rb +46 -72
  47. data/spec/integration/orders/valid_ids_spec.rb +6 -6
  48. data/spec/integration_helper.rb +0 -79
  49. data/spec/order_helper.rb +153 -0
  50. metadata +13 -4
@@ -34,12 +34,27 @@ module IB
34
34
  :exempt_code # int: ?
35
35
 
36
36
  DEFAULT_PROPS = {:con_id => 0,
37
- :ratio => 0,
38
37
  :open_close => SAME,
39
38
  :short_sale_slot => 0,
40
39
  :designated_location => '',
41
40
  :exempt_code => -1, }
42
41
 
42
+ # Leg's weight is a combination of action and ratio
43
+ def weight
44
+ action == 'BUY' ? ratio : -ratio
45
+ end
46
+
47
+ def weight= value
48
+ value = value.to_i
49
+ if value > 0
50
+ self.action = 'BUY'
51
+ self.ratio = value
52
+ else
53
+ self.action = 'SELL'
54
+ self.ratio = -value
55
+ end
56
+ end
57
+
43
58
  # Some messages include open_close, some don't. wtf.
44
59
  def serialize *fields
45
60
  [con_id,
@@ -20,10 +20,11 @@ module IB
20
20
 
21
21
  # This returns a Contract initialized from the serialize_ib_ruby format string.
22
22
  def self.from_ib_ruby string
23
- c = Contract.new
24
- c.symbol, c.sec_type, c.expiry, c.strike, c.right, c.multiplier,
25
- c.exchange, c.primary_exchange, c.currency, c.local_symbol = string.split(":")
26
- c
23
+ keys = [:symbol, :sec_type, :expiry, :strike, :right, :multiplier,
24
+ :exchange, :primary_exchange, :currency, :local_symbol]
25
+ props = Hash[keys.zip(string.split(":"))]
26
+ props.delete_if { |k, v| v.nil? || v.empty? }
27
+ Contract.new props
27
28
  end
28
29
 
29
30
  # Fields are Strings unless noted otherwise
@@ -62,7 +63,7 @@ module IB
62
63
  # non-aggregate (ie not the SMART) exchange that the contract trades on.
63
64
  proc { |val|
64
65
  val.upcase! if val.is_a?(String)
65
- raise(ArgumentError.new("Don't set primary_exchange to smart")) if val == 'SMART'
66
+ error "Don't set primary_exchange to smart", :args if val == 'SMART'
66
67
  self[:primary_exchange] = val
67
68
  },
68
69
 
@@ -77,7 +78,7 @@ module IB
77
78
  when 'CALL', 'C'
78
79
  'CALL'
79
80
  else
80
- raise ArgumentError.new("Invalid right '#{val}' (must be one of PUT, CALL, P, C)")
81
+ error "Right must be one of PUT, CALL, P, C - not '#{val}'", :args
81
82
  end
82
83
  },
83
84
 
@@ -90,7 +91,7 @@ module IB
90
91
  when nil, ''
91
92
  nil
92
93
  else
93
- raise ArgumentError.new("Invalid expiry '#{val}' (must be in format YYYYMM or YYYYMMDD)")
94
+ error "Invalid expiry '#{val}' (must be in format YYYYMM or YYYYMMDD)", :args
94
95
  end
95
96
  },
96
97
 
@@ -98,7 +99,7 @@ module IB
98
99
  proc { |val|
99
100
  val = nil if !val.nil? && val.empty?
100
101
  unless val.nil? || SECURITY_TYPES.values.include?(val)
101
- raise(ArgumentError.new("Invalid security type '#{val}' (must be one of #{SECURITY_TYPES.values}"))
102
+ error "Invalid security type '#{val}' (must be one of #{SECURITY_TYPES.values}", :args
102
103
  end
103
104
  self[:sec_type] = val
104
105
  }
@@ -154,6 +155,13 @@ module IB
154
155
  :under_delta, # double: The underlying stock or future delta.
155
156
  :under_price # double: The price of the underlying.
156
157
 
158
+ # Legs arriving via OpenOrder message, need to define them here
159
+ attr_accessor :legs # leg definitions for this contract.
160
+ alias combo_legs legs
161
+ alias combo_legs= legs=
162
+ alias combo_legs_description legs_description
163
+ alias combo_legs_description= legs_description=
164
+
157
165
  attr_accessor :description # NB: local to ib-ruby, not part of TWS.
158
166
 
159
167
  DEFAULT_PROPS = {:con_id => 0,
@@ -230,8 +238,8 @@ module IB
230
238
  # expiring in September, 2008, the string is:
231
239
  #
232
240
  # GBP:FUT:200809:::62500:GLOBEX::USD:
233
- def serialize_ib_ruby version
234
- serialize.join(":")
241
+ def serialize_ib_ruby
242
+ serialize_long.join(":")
235
243
  end
236
244
 
237
245
  # Contract comparison
@@ -13,16 +13,10 @@ module IB
13
13
  # The exception is for a STK legs, which must specify the SMART exchange.
14
14
  # 2. :symbol => "USD" For combo Contract, this is an arbitrary value (like �USD�)
15
15
 
16
- attr_reader :legs # leg definitions for this contract.
17
-
18
- alias combo_legs legs
19
- alias combo_legs_description legs_description
20
- alias combo_legs_description= legs_description=
21
-
22
16
  def initialize opts = {}
23
- super opts
24
17
  @legs = Array.new
25
18
  self[:sec_type] = IB::SECURITY_TYPES[:bag]
19
+ super opts
26
20
  end
27
21
 
28
22
  def description
@@ -20,6 +20,7 @@ module IB
20
20
  :cumulative_quantity, # int: Cumulative quantity. Used in regular
21
21
  # trades, combo trades and legs of the combo
22
22
  :liquidation, # int: This position is liquidated last should the need arise.
23
+ :order_ref, # int: Same order_ref as in corresponding Order
23
24
  [:account_name, :account_number], # String: The customer account number.
24
25
  :side => # String: Was the transaction a buy or a sale: BOT|SLD
25
26
  {:set => proc { |val| self[:side] = val.to_s.upcase[0..0] == 'B' ? :buy : :sell }}
@@ -34,7 +35,7 @@ module IB
34
35
  def to_s
35
36
  "<Execution #{time}: #{side} #{shares} @ #{price} on #{exchange}, " +
36
37
  "cumulative: #{cumulative_quantity} @ #{average_price}, " +
37
- "ids: #{order_id} order, #{perm_id} perm, #{exec_id} exec>"
38
+ "ids: #{exec_id} exec #{perm_id} perm #{order_id} order #{order_ref} ref>"
38
39
  end
39
40
  end # Execution
40
41
  end # module Models
@@ -14,7 +14,7 @@ module IB
14
14
  # If a opts hash is given, keys are taken as attribute names, values as data.
15
15
  # The model instance fields are then set automatically from the opts Hash.
16
16
  def initialize(opts={})
17
- raise ArgumentError.new("Argument must be a Hash") unless opts.is_a?(Hash)
17
+ error "Argument must be a Hash", :args unless opts.is_a?(Hash)
18
18
  @created_at = Time.now
19
19
 
20
20
  props = self.class::DEFAULT_PROPS.merge(opts)
@@ -42,9 +42,6 @@ module IB
42
42
  Volatility_Ref_Price_Average = 1
43
43
  Volatility_Ref_Price_BidOrAsk = 2
44
44
 
45
- # No idea why IB uses a large number as the default for some fields
46
- Max_Value = 99999999
47
-
48
45
  # Main order fields
49
46
  prop :order_id, # int: Order id associated with client (volatile).
50
47
  :client_id, # int: The id of the client that placed this order.
@@ -106,7 +103,7 @@ module IB
106
103
  # GTD Good-till-Date/Time
107
104
  # GTC Good-till-Canceled
108
105
  # IOC Immediate-or-Cancel
109
- :oca_group, # String: one cancels all group name
106
+ :oca_group, # String: Identifies a member of a one-cancels-all group.
110
107
  :oca_type, # int: Tells how to handle remaining orders in an OCA group
111
108
  # when one order or part of an order executes. Valid values:
112
109
  # - 1 = Cancel all remaining orders with block
@@ -144,14 +141,20 @@ module IB
144
141
  # and the spread between the bid and ask must be less than
145
142
  # 0.1% of the midpoint
146
143
 
144
+ :what_if, # bool: Use to request pre-trade commissions and margin
145
+ # information. If set to true, margin and commissions data is received
146
+ # back via the OrderState() object for the openOrder() callback.
147
+ :not_held, # public boolean m_notHeld; // Not Held
147
148
  :outside_rth, # bool: allows orders to also trigger or fill outside
148
149
  # of regular trading hours. (WAS: ignore_rth)
149
150
  :hidden, # bool: the order will not be visible when viewing
150
151
  # the market depth. Only for ISLAND exchange.
151
- :good_after_time, # FORMAT: 20060505 08:00:00 {time zone}
152
- # Use an empty String if not applicable.
153
- :good_till_date, # FORMAT: 20060505 08:00:00 {time zone}
154
- # Use an empty String if not applicable.
152
+ :good_after_time, # Indicates that the trade should be submitted after the
153
+ # time and date set, format YYYYMMDD HH:MM:SS (seconds are optional).
154
+ :good_till_date, # Indicates that the trade should remain working until the
155
+ # time and date set, format YYYYMMDD HH:MM:SS (seconds are optional).
156
+ # You must set the :tif to GTD when using this string.
157
+ # Use an empty String if not applicable.
155
158
  :override_percentage_constraints, # bool: Precautionary constraints defined on
156
159
  # the TWS Presets page ensure that your price and size order values are reasonable.
157
160
  # Orders sent from the API are also validated against these safety constraints,
@@ -164,6 +167,8 @@ module IB
164
167
  :min_quantity, # int: Identifies a minimum quantity order type.
165
168
  :percent_offset, # double: percent offset amount for relative (REL)orders only
166
169
  :trail_stop_price, # double: for TRAILLIMIT orders only
170
+ # As of client v.56, we receive trailing_percent in openOrder
171
+ :trailing_percent,
167
172
 
168
173
  # Financial advisors only - use an empty String if not applicable.
169
174
  :fa_group, :fa_profile, :fa_method, :fa_percentage,
@@ -171,7 +176,7 @@ module IB
171
176
  # Institutional orders only!
172
177
  :open_close, # String: O=Open, C=Close
173
178
  :origin, # 0=Customer, 1=Firm
174
- :order_ref, # String: The order reference. For institutional customers only.
179
+ :order_ref, # String: Order reference. Customer defined order ID tag.
175
180
  :short_sale_slot, # 1 - you hold the shares,
176
181
  # 2 - they will be delivered from elsewhere.
177
182
  # Only for Action="SSHORT
@@ -192,6 +197,7 @@ module IB
192
197
  :etrade_only, # bool: Trade with electronic quotes.
193
198
  :firm_quote_only, # bool: Trade with firm quotes.
194
199
  :nbbo_price_cap, # double: Maximum Smart order distance from the NBBO.
200
+ :opt_out_smart_routing, # Australian exchange only, default false
195
201
 
196
202
  # BOX or VOL ORDERS ONLY
197
203
  :auction_strategy, # For BOX exchange only. Valid values:
@@ -219,38 +225,61 @@ module IB
219
225
  # - 1 = Average of National Best Bid or Ask,
220
226
  # - 2 = National Best Bid when buying a call or selling a put;
221
227
  # and National Best Ask when selling a call or buying a put.
228
+ :continuous_update, # int: Used for dynamic management of volatility orders.
229
+ # Determines whether TWS is supposed to update the order price as the underlying
230
+ # moves. If selected, the limit price sent to an exchange is modified by TWS
231
+ # if the computed price of the option changes enough to warrant doing so. This
232
+ # is helpful in keeping the limit price up to date as the underlying price changes.
222
233
  :delta_neutral_order_type, # String: Enter an order type to instruct TWS
223
234
  # to submit a delta neutral trade on full or partial execution of the
224
235
  # VOL order. For no hedge delta order to be sent, specify NONE.
225
236
  :delta_neutral_aux_price, # double: Use this field to enter a value if
226
237
  # the value in the deltaNeutralOrderType field is an order
227
238
  # type that requires an Aux price, such as a REL order.
228
- :continuous_update, # int: Used for dynamic management of volatility orders.
229
- # Determines whether TWS is supposed to update the order price as the underlying
230
- # moves. If selected, the limit price sent to an exchange is modified by TWS
231
- # if the computed price of the option changes enough to warrant doing so. This
232
- # is helpful in keeping the limit price up to date as the underlying price changes.
239
+
240
+ # As of client v.52, we also receive delta... params in openOrder
241
+ :delta_neutral_con_id,
242
+ :delta_neutral_settling_firm,
243
+ :delta_neutral_clearing_account,
244
+ :delta_neutral_clearing_intent,
245
+
246
+ # HEDGE ORDERS ONLY:
247
+ # As of client v.49/50, we can now add hedge orders using the API.
248
+ # Hedge orders are child orders that take additional fields. There are four
249
+ # types of hedging orders supported by the API: Delta, Beta, FX, Pair.
250
+ # All hedge orders must have a parent order submitted first. The hedge order
251
+ # should set its :parent_id. If the hedgeType is Beta, the beta sent in the
252
+ # hedgeParm can be zero, which means it is not used. Delta is only valid
253
+ # if the parent order is an option and the child order is a stock.
254
+
255
+ :hedge_type, # String: D = Delta, B = Beta, F = FX or P = Pair
256
+ :hedge_param, # String; value depends on the hedgeType; sent from the API
257
+ # only if hedge_type is NOT null. It is required for Pair hedge order,
258
+ # optional for Beta hedge orders, and ignored for Delta and FX hedge orders.
233
259
 
234
260
  # COMBO ORDERS ONLY:
235
261
  :basis_points, # double: EFP orders only
236
262
  :basis_points_type, # double: EFP orders only
237
263
 
238
- # SCALE ORDERS ONLY
239
- :scale_init_level_size, # int: Size of the first (initial) order component.
240
- :scale_subs_level_size, # int: Order size of the subsequent scale order
241
- # components. Used in conjunction with scaleInitLevelSize().
242
- :scale_price_increment, # double: Defines the price increment between
243
- # scale components. This field is required for Scale orders.
244
-
245
- # ALGO ORDERS ONLY
264
+ # ALGO ORDERS ONLY:
246
265
  :algo_strategy, # String
247
266
  :algo_params, # public Vector<TagValue> m_algoParams; ?!
248
267
 
249
- # WTF?!
250
- :what_if, # bool: Use to request pre-trade commissions and margin
251
- # information. If set to true, margin and commissions data is received
252
- # back via the OrderState() object for the openOrder() callback.
253
- :not_held # public boolean m_notHeld; // Not Held
268
+ # SCALE ORDERS ONLY:
269
+ :scale_init_level_size, # int: Size of the first (initial) order component.
270
+ :scale_subs_level_size, # int: Order size of the subsequent scale order
271
+ # components. Used in conjunction with scaleInitLevelSize().
272
+ :scale_price_increment, # double: Price increment between scale components.
273
+ # This field is required for Scale orders.
274
+
275
+ # As of client v.54, we can receive additional scale order fields:
276
+ :scale_price_adjust_value,
277
+ :scale_price_adjust_interval,
278
+ :scale_profit_offset,
279
+ :scale_auto_reset,
280
+ :scale_init_position,
281
+ :scale_init_fill_qty,
282
+ :scale_random_percent
254
283
 
255
284
  # Some Order properties (received back from IB) are separated into
256
285
  # OrderState object. Here, they are lumped into Order proper: see OrderState.java
@@ -283,11 +312,9 @@ module IB
283
312
  # the order is inactive due to system, exchange or other issues.
284
313
  :commission, # double: Shows the commission amount on the order.
285
314
  :commission_currency, # String: Shows the currency of the commission.
286
-
287
315
  #The possible range of the actual order commission:
288
316
  :min_commission,
289
317
  :max_commission,
290
-
291
318
  :warning_text, # String: Displays a warning message if warranted.
292
319
 
293
320
  # String: Shows the impact the order would have on your initial margin.
@@ -299,6 +326,13 @@ module IB
299
326
  # String: Shows the impact the order would have on your equity with loan value.
300
327
  :equity_with_loan => proc { |val| self[:equity_with_loan] = filter_max val }
301
328
 
329
+
330
+ # Returned in OpenOrder for Bag Contracts
331
+ # public Vector<OrderComboLeg> m_orderComboLegs
332
+ attr_accessor :leg_prices, :combo_params
333
+ alias order_combo_legs leg_prices
334
+ alias smart_combo_routing_params combo_params
335
+
302
336
  # IB uses weird String with Java Double.MAX_VALUE to indicate no value here
303
337
  def filter_max val
304
338
  val == "1.7976931348623157E308" ? nil : val.to_f
@@ -308,31 +342,41 @@ module IB
308
342
  :parent_id => 0,
309
343
  :tif => 'DAY',
310
344
  :outside_rth => false,
311
- :open_close => "O",
345
+ :open_close => 'O',
312
346
  :origin => Origin_Customer,
313
347
  :transmit => true,
314
348
  :designated_location => '',
315
349
  :exempt_code => -1,
316
350
  :delta_neutral_order_type => '',
351
+ :delta_neutral_con_id => 0,
352
+ :delta_neutral_settling_firm => '',
353
+ :delta_neutral_clearing_account => '',
354
+ :delta_neutral_clearing_intent => '',
355
+ :algo_strategy => '',
317
356
  :what_if => false,
318
357
  :not_held => false,
319
- :algo_strategy => '', }
358
+ :scale_auto_reset => false,
359
+ :scale_random_percent => false,
360
+ :opt_out_smart_routing => false,
361
+ }
320
362
 
321
363
  def initialize opts = {}
322
- @algo_params = []
364
+ @leg_prices = []
365
+ @algo_params = {}
366
+ @combo_params = {}
323
367
  super opts
324
368
  end
325
369
 
326
370
  # This returns an Array of data from the given order,
327
371
  # mixed with data from associated contract. Ugly mix, indeed.
328
- def serialize_with contract
372
+ def serialize_with server, contract
329
373
  [contract.serialize_long(:con_id, :sec_id),
330
374
  action, # main order fields
331
375
  total_quantity,
332
376
  order_type,
333
377
  limit_price,
334
378
  aux_price,
335
- tif, # xtended order fields
379
+ tif, # extended order fields
336
380
  oca_group,
337
381
  account,
338
382
  open_close,
@@ -347,6 +391,21 @@ module IB
347
391
  outside_rth, # was: ignore_rth
348
392
  hidden,
349
393
  contract.serialize_legs(:extended),
394
+
395
+ # Support for per-leg prices in Order
396
+ if server[:server_version] >= 61
397
+ leg_prices.empty? ? 0 : [leg_prices.size] + leg_prices
398
+ else
399
+ []
400
+ end,
401
+
402
+ # Support for combo routing params in Order
403
+ if server[:server_version] >= 57 && contract.sec_type == 'BAG'
404
+ combo_params.empty? ? 0 : [combo_params.size] + combo_params.to_a
405
+ else
406
+ []
407
+ end,
408
+
350
409
  '', # deprecated shares_allocation field
351
410
  discretionary_amount,
352
411
  good_after_time,
@@ -357,32 +416,33 @@ module IB
357
416
  fa_profile,
358
417
  short_sale_slot, # 0 only for retail, 1 or 2 for institution (Institutional)
359
418
  designated_location, # only populate when short_sale_slot == 2 (Institutional)
419
+ exempt_code,
360
420
  oca_type,
361
421
  rule_80a,
362
422
  settling_firm,
363
423
  all_or_none,
364
- min_quantity || EOL,
365
- percent_offset || EOL,
424
+ min_quantity,
425
+ percent_offset,
366
426
  etrade_only,
367
427
  firm_quote_only,
368
- nbbo_price_cap || EOL,
369
- auction_strategy || EOL,
370
- starting_price || EOL,
371
- stock_ref_price || EOL,
372
- delta || EOL,
373
- stock_range_lower || EOL,
374
- stock_range_upper || EOL,
428
+ nbbo_price_cap,
429
+ auction_strategy,
430
+ starting_price,
431
+ stock_ref_price,
432
+ delta,
433
+ stock_range_lower,
434
+ stock_range_upper,
375
435
  override_percentage_constraints,
376
- volatility || EOL, # Volatility orders
377
- volatility_type || EOL, # Volatility orders
378
- delta_neutral_order_type, # Volatility orders
379
- delta_neutral_aux_price || EOL, # Volatility orders
380
- continuous_update, # Volatility orders
381
- reference_price_type || EOL, # Volatility orders
382
- trail_stop_price || EOL, # TRAIL_STOP_LIMIT stop price
383
- scale_init_level_size || EOL, # Scale Orders
384
- scale_subs_level_size || EOL, # Scale Orders
385
- scale_price_increment || EOL, # Scale Orders
436
+ volatility, # Volatility orders
437
+ volatility_type, # Volatility orders
438
+ delta_neutral_order_type, # Volatility orders
439
+ delta_neutral_aux_price, # Volatility orders
440
+ continuous_update, # Volatility orders
441
+ reference_price_type, # Volatility orders
442
+ trail_stop_price, # TRAIL_STOP_LIMIT stop price
443
+ scale_init_level_size, # Scale Orders
444
+ scale_subs_level_size, # Scale Orders
445
+ scale_price_increment, # Scale Orders
386
446
  clearing_account,
387
447
  clearing_intent,
388
448
  not_held,
@@ -393,7 +453,7 @@ module IB
393
453
 
394
454
  def serialize_algo
395
455
  if algo_strategy.nil? || algo_strategy.empty?
396
- ['']
456
+ ''
397
457
  else
398
458
  [algo_strategy,
399
459
  algo_params.size,
@@ -403,9 +463,9 @@ module IB
403
463
 
404
464
  # Order comparison
405
465
  def == other
406
- perm_id && perm_id == other.perm_id ||
466
+ perm_id && other.perm_id && perm_id == other.perm_id ||
407
467
  order_id == other.order_id && # ((p __LINE__)||true) &&
408
- client_id == other.client_id &&
468
+ (client_id == other.client_id || client_id == 0 || other.client_id == 0) &&
409
469
  parent_id == other.parent_id &&
410
470
  tif == other.tif &&
411
471
  action == other.action &&