ib-ruby 0.6.1 → 0.7.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 (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 &&