ib-api 972.5 → 972.5.2

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.
data/lib/ib/connection.rb CHANGED
@@ -35,76 +35,81 @@ module IB
35
35
  alias next_order_id= next_local_id=
36
36
 
37
37
  def initialize host: '127.0.0.1',
38
- port: '4002', # IB Gateway connection (default --> demo) 4001: production
39
- #:port => '7497', # TWS connection --> demo 7496: production
40
- connect: true, # Connect at initialization
41
- received: true, # Keep all received messages in a @received Hash
42
- # redis: false, # future plans
43
- logger: nil,
44
- client_id: rand( 1001 .. 9999 ) ,
45
- client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
46
- optional_capacities: "", # TWS-Version 974: "+PACEAPI"
47
- #server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
48
- **any_other_parameters_which_are_ignored
49
- # V 974 release motes
50
- # API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
51
-
52
- self.class.configure_logger logger
53
- # convert parameters into instance-variables and assign them
54
- method(__method__).parameters.each do |type, k|
55
- next unless type == :key ## available: key , keyrest
56
- next if k.to_s == 'logger'
57
- v = eval(k.to_s)
58
- instance_variable_set("@#{k}", v) unless v.nil?
59
- end
60
-
61
- # A couple of locks to avoid race conditions in JRuby
62
- @subscribe_lock = Mutex.new
63
- @receive_lock = Mutex.new
64
- @message_lock = Mutex.new
65
-
66
- @connected = false
67
- self.next_local_id = nil
38
+ port: '4002', # IB Gateway connection (default --> demo) 4001: production
39
+ #:port => '7497', # TWS connection --> demo 7496: production
40
+ connect: true, # Connect at initialization
41
+ received: true, # Keep all received messages in a @received Hash
42
+ # redis: false, # future plans
43
+ logger: nil,
44
+ client_id: rand( 1001 .. 9999 ) ,
45
+ client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
46
+ optional_capacities: "", # TWS-Version 974: "+PACEAPI"
47
+ #server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
48
+ **any_other_parameters_which_are_ignored
49
+ # V 974 release motes
50
+ # API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
51
+
52
+ self.class.configure_logger logger
53
+ # convert parameters into instance-variables and assign them
54
+ method(__method__).parameters.each do |type, k|
55
+ next unless type == :key ## available: key , keyrest
56
+ next if k.to_s == 'logger'
57
+ v = eval(k.to_s)
58
+ instance_variable_set("@#{k}", v) unless v.nil?
59
+ end
68
60
 
69
- # self.subscribe(:Alert) do |msg|
70
- # puts msg.to_human
71
- # end
61
+ # A couple of locks to avoid race conditions in JRuby
62
+ @subscribe_lock = Mutex.new
63
+ @receive_lock = Mutex.new
64
+ @message_lock = Mutex.new
72
65
 
73
- # TWS always sends NextValidId message at connect -subscribe save this id
74
- ## this block is executed before tws-communication is established
75
- yield self if block_given?
66
+ @connected = false
67
+ self.next_local_id = nil
76
68
 
77
- self.subscribe(:NextValidId) do |msg|
78
- self.logger.progname = "Connection#connect"
79
- self.next_local_id = msg.local_id
80
- self.logger.info { "Got next valid order id: #{next_local_id}." }
81
- end
82
-
83
- # Ensure the transmission of NextValidId.
84
- # works even if no reader_thread is established
85
- if connect
86
- disconnect if connected?
87
- update_next_order_id
88
- Kernel.exit if self.next_local_id.nil?
89
- end
90
- #start_reader if @received && connected?
91
- Connection.current = self
92
- end
69
+ # TWS always sends NextValidId message at connect -subscribe save this id
70
+ self.subscribe(:NextValidId) do |msg|
71
+ self.logger.progname = "Connection#connect"
72
+ self.next_local_id = msg.local_id
73
+ self.logger.info { "Got next valid order id: #{next_local_id}." }
74
+ end
75
+ #
76
+ # this block is executed before tws-communication is established
77
+ # Its intended for globally available subscriptions of tws-messages
78
+ yield self if block_given?
79
+
80
+ if connect
81
+ disconnect if connected?
82
+ update_next_order_id
83
+ Kernel.exit if self.next_local_id.nil? # emergency exit.
84
+ # update_next_order_id should have raised an error
85
+ end
86
+ Connection.current = self
87
+ end
93
88
 
94
89
  # read actual order_id and
95
90
  # connect if not connected
96
91
  def update_next_order_id
97
- i,finish = 0, false
98
- sub = self.subscribe(:NextValidID) { finish = true }
99
- connected? ? self.send_message( :RequestIds ) : open()
100
- Timeout::timeout(1, IB::TransmissionError,"Could not get NextValidId" ) do
101
- loop { sleep 0.1; break if finish }
102
- end
103
- self.unsubscribe sub
104
- end
92
+ q = Queue.new
93
+ subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id }
94
+ unless connected?
95
+ connect() # connect implies requesting NextValidId
96
+ else
97
+ send_message :RequestIds
98
+ end
99
+ th = Thread.new { sleep 5; q.close }
100
+ local_id = q.pop
101
+ if q.closed?
102
+ error "Could not get NextValidID", :reader
103
+ else
104
+ th.kill
105
+ end
106
+ unsubscribe subscription
107
+ local_id # return next_id
108
+ end
105
109
 
106
110
  ### Working with connection
107
-
111
+ #
112
+ ### connect can be called directly. but is mostly called through update_next_order_id
108
113
  def connect
109
114
  logger.progname='IB::Connection#connect'
110
115
  if connected?
@@ -123,10 +128,6 @@ module IB
123
128
  @local_connect_time = Time.now
124
129
  end
125
130
 
126
- # Sending (arbitrary) client ID to identify subsequent communications.
127
- # The client with a client_id of 0 can manage the TWS-owned open orders.
128
- # Other clients can only manage their own open orders.
129
-
130
131
  # V100 initial handshake
131
132
  # Parameters borrowed from the python client
132
133
  start_api = 71
@@ -134,14 +135,11 @@ module IB
134
135
  # optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
135
136
  socket.send_messages start_api, version, @client_id , @optional_capacities
136
137
  @connected = true
137
- logger.info { "Connected to server, version: #{@server_version},\n connection time: " +
138
+ logger.fatal{ "Connected to server, version: #{@server_version}, " +
139
+ "using client-id: #{client_id},\n connection time: " +
138
140
  "#{@local_connect_time} local, " +
139
- "#{@remote_connect_time} remote."}
141
+ "#{@remote_connect_time} remote." }
140
142
 
141
- # if the client_id is wrong or the port is not accessible the first read attempt fails
142
- # get the first message and proceed if something reasonable is recieved
143
- the_message = process_message # recieve next_order_id
144
- error "Check Port/Client_id ", :reader if the_message == " "
145
143
  start_reader
146
144
  end
147
145
 
@@ -184,7 +182,7 @@ module IB
184
182
  when what.is_a?(Symbol)
185
183
  if Messages::Incoming.const_defined?(what)
186
184
  [Messages::Incoming.const_get(what)]
187
- elsif TechnicalAnalysis::Signals.const_defined?(what)
185
+ elsif defined?( TechnicalAnalysis ) && TechnicalAnalysis::Signals.const_defined?(what)
188
186
  [TechnicalAnalysis::Signals.const_get?(what)]
189
187
  else
190
188
  error "#{what} is no IB::Messages or TechnicalAnalyis::Signals class"
@@ -258,6 +256,7 @@ module IB
258
256
  #
259
257
  # wait_for depends heavyly on Connection#received. If collection of messages through recieved
260
258
  # is turned off, wait_for loses most of its functionality
259
+
261
260
  def wait_for *args, &block
262
261
  timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
263
262
  end_time = Time.now + (timeout || 1) # default timeout 1 sec
@@ -282,25 +281,40 @@ module IB
282
281
  # Process incoming messages during *poll_time* (200) msecs, nonblocking
283
282
  def process_messages poll_time = 50 # in msec
284
283
  time_out = Time.now + poll_time/1000.0
284
+ begin
285
285
  while (time_left = time_out - Time.now) > 0
286
286
  # If socket is readable, process single incoming message
287
- #process_message if select [socket], nil, nil, time_left
287
+ if RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/)
288
+ process_message if select [socket], nil, nil, time_left
289
+
290
+
288
291
  # the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
289
292
  # unfortunately, it raises Errors in windows environment
290
- # disabled for now
291
- if select [socket], nil, nil, time_left
292
- # # Peek at the message from the socket; if it's blank then the
293
- # # server side of connection (TWS) has likely shut down.
294
- socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == ""
295
- #
296
- # # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
297
- process_message
298
- #
299
- # # After processing, if socket has shut down we sleep for 100ms
300
- # # to avoid spinning in a tight loop. If the server side somehow
301
- # # comes back up (gets reconnedted), normal processing
302
- # # (without the 100ms wait) should happen.
303
- sleep(0.1) if socket_likely_shutdown
293
+ else
294
+ if select [socket], nil, nil, time_left
295
+ # # Peek at the message from the socket; if it's blank then the
296
+ # # server side of connection (TWS) has likely shut down.
297
+ socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == ""
298
+ #
299
+ # # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
300
+ process_message
301
+ #
302
+ # # After processing, if socket has shut down we sleep for 100ms
303
+ # # to avoid spinning in a tight loop. If the server side somehow
304
+ # # comes back up (gets reconnedted), normal processing
305
+ # # (without the 100ms wait) should happen.
306
+ sleep(0.1) if socket_likely_shutdown
307
+ end
308
+ end
309
+ end
310
+ rescue Errno::ECONNRESET => e
311
+ logger.fatal e.message
312
+ if e.message =~ /Connection reset by peer/
313
+ logger.fatal "Is another client listening on the same port?"
314
+ error "try reconnecting with a different client-id", :reader
315
+ else
316
+ logger.fatal "Aborting"
317
+ Kernel.exit
304
318
  end
305
319
  end
306
320
  end
@@ -374,15 +388,20 @@ module IB
374
388
  # If you don't start reader, you should manually poll @socket for messages
375
389
  # or use #process_messages(msec) API.
376
390
  def start_reader
377
- return(@reader_thread) if @reader_running
378
- if connected?
379
- Thread.abort_on_exception = true
380
- @reader_running = true
381
- @reader_thread = Thread.new { process_messages while @reader_running }
382
- else
383
- logger.fatal {"Could not start reader, not connected!"}
384
- nil # return_value
385
- end
391
+ if @reader_running
392
+ @reader_thread
393
+ elsif connected?
394
+ begin
395
+ Thread.abort_on_exception = true
396
+ @reader_running = true
397
+ @reader_thread = Thread.new { process_messages while @reader_running }
398
+ rescue Errno::ECONNRESET => e
399
+ logger.fatal e.message
400
+ Kernel.exit
401
+ end
402
+ else
403
+ error "Could not start reader, not connected!", :reader, true
404
+ end
386
405
  end
387
406
 
388
407
  protected
@@ -408,13 +427,16 @@ module IB
408
427
  # Create new instance of the appropriate message type,
409
428
  # and have it read the message from socket.
410
429
  # NB: Failure here usually means unsupported message type received
411
- logger.error { "Got unsupported message #{msg_id}" } unless Messages::Incoming::Classes[msg_id]
412
- error "Something strange happened - Reader has to be restarted" , :reader if msg_id.to_i.zero?
413
- msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
430
+ unless Messages::Incoming::Classes[msg_id]
431
+ logger.error { "Got unsupported message #{msg_id}" }
432
+ error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero?
433
+ else
434
+ msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
435
+ end
414
436
 
415
437
  # Deliver message to all registered subscribers, alert if no subscribers
416
438
  # Ruby 2.0 and above: Hashes are ordered.
417
- # Thus first declared subscribers of a class are executed first
439
+ # Thus first declared subscribers of a class are executed first
418
440
  @subscribe_lock.synchronize do
419
441
  subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
420
442
  end
data/lib/ib/constants.rb CHANGED
@@ -21,7 +21,11 @@ module IB
21
21
  '15 mins' =>:min15,
22
22
  '30 mins' =>:min30,
23
23
  '1 hour' =>:hour1,
24
- '1 day' => :day1
24
+ '4 hours' =>:hour4,
25
+ '8 hours' =>:hour8,
26
+ '1 day' => :day1,
27
+ '1 week' => :week1,
28
+ '1 month' => :month1,
25
29
  }.freeze
26
30
 
27
31
  # Enumeration of data types.
@@ -199,7 +203,6 @@ module IB
199
203
  'PEG MID' => :pegged_to_midpoint, # Pegged-to-Midpoint
200
204
  'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benmchmark # Vers. 102
201
205
  'VWAP' => :vwap, # VWAP-Guaranted
202
- 'OCA' => :one_cancels_all, # One-Cancels-All
203
206
  'VOL' => :volatility, # Volatility
204
207
  'SCALE' => :scale, # Scale
205
208
  'NONE' => :none, # Used to indicate no hedge in :delta_neutral_order_type
data/lib/ib/errors.rb CHANGED
@@ -38,7 +38,9 @@ def error message, type=:standard, backtrace=nil
38
38
  IB::FlexError.new message
39
39
  when :reader
40
40
  IB::TransmissionError.new message
41
+ when :verify
42
+ IB::VerifyError.new message
41
43
  end
42
- e.set_backtrace(backtrace) if backtrace
44
+ e.set_backtrace(caller) if backtrace
43
45
  raise e
44
46
  end
@@ -4,10 +4,10 @@ module IB
4
4
 
5
5
  # Called Error in Java code, but in fact this type of messages also
6
6
  # deliver system alerts and additional (non-error) info from TWS.
7
- ErrorMessage = Error = Alert = def_message([4, 2],
8
- [:error_id, :int],
9
- [:code, :int],
10
- [:message, :string])
7
+ Alert = def_message([4, 2],
8
+ [:error_id, :int],
9
+ [:code, :int],
10
+ [:message, :string])
11
11
  class Alert
12
12
  # Is it an Error message?
13
13
  def error?
@@ -83,7 +83,7 @@ module IB
83
83
  TickGeneric = def_message [45, 6], AbstractTick,
84
84
  [:ticker_id, :int],
85
85
  [:tick_type, :int],
86
- [:value, :decimal]
86
+ [:value, :float]
87
87
 
88
88
  TickString = def_message [46, 6], AbstractTick,
89
89
  [:ticker_id, :int],
@@ -62,7 +62,7 @@ module IB
62
62
  bar_size,
63
63
  data_type.to_s.upcase,
64
64
  @data[:use_rth] ,
65
- "XYZ" # not suported realtimebars option string
65
+ "" # not suported realtimebars option string
66
66
  ]
67
67
  end
68
68
  end # RequestRealTimeBars
@@ -189,7 +189,7 @@ module IB
189
189
  2 , # @data[:format_date], format-date is hard-coded as int_date in incoming/historicalData
190
190
  contract.serialize_legs ,
191
191
  @data[:keep_up_todate], # 0 / 1
192
- 'XYZ' # chartOptions:TagValueList - For internal use only. Use default value XYZ.
192
+ '' # chartOptions:TagValueList - For internal use only. Use default value XYZ.
193
193
  ]
194
194
  end
195
195
  end # RequestHistoricalData
@@ -74,9 +74,9 @@ module IB
74
74
  order.all_or_none || false,
75
75
  order.min_quantity || "",
76
76
  order.percent_offset || '',
77
- order.etrade_only || false,
78
- order.firm_quote_only || false,
79
- order.nbbo_price_cap || "",
77
+ false, # was: order.etrade_only || false, desupported in TWS > 981
78
+ false, # was: order.firm_quote_only || false, desupported in TWS > 981
79
+ order.nbbo_price_cap || "", ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here
80
80
  order[:auction_strategy],
81
81
  order.starting_price,
82
82
  order.stock_ref_price || "",
@@ -96,7 +96,7 @@ module IB
96
96
  [:tick_list, ->(tick_list){ tick_list.is_a?(Array) ? tick_list.join(',') : (tick_list || '')}, []],
97
97
  [:snapshot, false],
98
98
  [:regulatory_snapshot, false],
99
- [:mkt_data_options, "XYZ"]
99
+ [:mkt_data_options, ""]
100
100
  end
101
101
  end
102
102
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IB
4
+ # Convert data passed in from a TCP socket stream, and convert into
5
+ # raw messages. The messages
6
+ class RawMessageParser
7
+ HEADER_LNGTH = 4
8
+ def initialize(socket)
9
+ @socket = socket
10
+ @data = String.new
11
+ end
12
+
13
+ def each
14
+ while true
15
+ append_new_data
16
+
17
+ next unless length_data?
18
+ next unless enough_data?
19
+
20
+ length = next_msg_length
21
+ validate_data_header(length)
22
+
23
+ raw = grab_message(length)
24
+ validate_message_footer(raw, length)
25
+ msg = parse_message(raw, length)
26
+ remove_message
27
+ yield msg
28
+ end
29
+ end
30
+
31
+ # extract message and convert to
32
+ # an array split by null characters.
33
+ def grab_message(length)
34
+ @data.byteslice(HEADER_LNGTH, length)
35
+ end
36
+
37
+ def parse_message(raw, length)
38
+ raw.unpack1("A#{length}").split("\0")
39
+ end
40
+
41
+ def remove_message
42
+ length = next_msg_length
43
+ leftovers = @data.byteslice(length + HEADER_LNGTH..-1)
44
+ @data = if leftovers.nil?
45
+ String.new
46
+ else
47
+ leftovers
48
+ end
49
+ end
50
+
51
+ def enough_data?
52
+ actual_lngth = next_msg_length + HEADER_LNGTH
53
+ echo 'too little data' if next_msg_length.nil?
54
+ return false if next_msg_length.nil?
55
+ @data.bytesize >= actual_lngth
56
+ end
57
+
58
+ def length_data?
59
+ @data.bytesize > HEADER_LNGTH
60
+ end
61
+
62
+ def next_msg_length
63
+ #can't check length if first 4 bytes don't exist
64
+ length = @data.byteslice(0..3).unpack1('N')
65
+ return 0 if length.nil?
66
+ length
67
+ end
68
+
69
+ def append_new_data
70
+ @data += @socket.recv_from
71
+ end
72
+
73
+ def validate_message_footer(msg,length)
74
+ last = msg.bytesize
75
+ last_byte = msg.byteslice(last-1,last)
76
+ raise 'Could not validate last byte' if last_byte.nil?
77
+ raise "Message has an invalid last byte. expecting \0, received: #{last_byte}" if last_byte != "\0"
78
+ end
79
+
80
+ def validate_data_header(length)
81
+ return true if length <= 5000
82
+ raise 'Message is longer than sane max length'
83
+ end
84
+ end
85
+ end
data/lib/ib/socket.rb CHANGED
@@ -154,7 +154,7 @@ module IB
154
154
  def send_messages *data
155
155
  self.syswrite prepare_message(data)
156
156
  rescue Errno::ECONNRESET => e
157
- Connection.logger.error{ "Data not accepted by IB \n
157
+ Connection.logger.fatal{ "Data not accepted by IB \n
158
158
  #{data.inspect} \n
159
159
  Backtrace:\n "}
160
160
  Connection.logger.error e.backtrace
@@ -172,7 +172,7 @@ module IB
172
172
  end while buffer.size == 4096
173
173
  complete_message_buffer.join('')
174
174
  rescue Errno::ECONNRESET => e
175
- Connection.logger.error{ "Data Buffer is not filling \n
175
+ Connection.logger.fatal{ "Data Buffer is not filling \n
176
176
  The Buffer: #{buffer.inspect} \n
177
177
  Backtrace:\n
178
178
  #{e.backtrace.join("\n") } " }
data/lib/logging.rb CHANGED
@@ -26,20 +26,21 @@ module Support
26
26
  @logger = logger
27
27
  end
28
28
 
29
- def configure_logger(log=nil)
30
- if log
29
+ def configure_logger(log= STDOUT)
30
+ if log.is_a? Logger
31
31
  @logger = log
32
32
  else
33
- @logger = Logger.new(STDOUT)
34
- @logger.level = Logger::INFO
35
- @logger.formatter = proc do |severity, datetime, progname, msg|
33
+ @logger = Logger.new log
34
+ end
35
+ @logger.level = Logger::INFO
36
+ @logger.formatter = proc do |severity, datetime, progname, msg|
36
37
  # "#{datetime.strftime("%d.%m.(%X)")}#{"%5s" % severity}->#{msg}\n"
37
- "#{"%5s" % severity}::#{msg}\n"
38
+ "#{"%1s" % severity[0]}: #{msg}\n"
38
39
  end
39
- @logger.debug "------------------------------ start logging ----------------------------"
40
- end # branch
40
+ @logger.debug "------------------------------ start logging ----------------------------"
41
41
  end # def
42
42
  end # module ClassMethods
43
43
  end # module Logging
44
44
  end # module Support
45
45
 
46
+ # source: https://github.com/jondot/sneakers/blob/master/lib/sneakers/concerns/logging.rb
@@ -5,7 +5,7 @@ require 'models/ib/underlying'
5
5
 
6
6
  module IB
7
7
 
8
- if defined?(Contract)
8
+ if defined?(Contract)
9
9
  #Connection.current.logger.warn "Contract already a #{defined?(Contract)}"
10
10
 
11
11
  # puts Contract.ancestors
@@ -235,10 +235,46 @@ module IB
235
235
 
236
236
  # creates a new Contract substituting attributes by the provided key-value pairs.
237
237
  #
238
- # con_id is resetted
239
- def merge **new_attributes
240
- self.con_id = 0
241
- self.class.new attributes.merge new_attributes
238
+ # for convenience
239
+ # con_id, local_symbol and last_trading_day are resetted,
240
+ # the link to contract-details is savaged
241
+ #
242
+ # Example
243
+ # ge = Stock.new( symbol: :ge).verify.first
244
+ # f = ge.merge symbol: :f
245
+ #
246
+ # c = Contract.new( con_id: 428520002, exchange: 'Globex')
247
+ #puts c.verify.as_table
248
+ #┌────────┬────────┬───────────┬──────────┬──────────┬────────────┬───────────────┬───────┬────────┬──────────┐
249
+ #│ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │
250
+ #╞════════╪════════╪═══════════╪══════════╪══════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡
251
+ #│ Future │ NQ │ 428520002 │ GLOBEX │ 20210917 │ 20 │ NQ │ │ │ USD │
252
+ #└────────┴────────┴───────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘
253
+ # d= c.merge symbol: :es, trading_class: '', multiplier: 50
254
+ # puts d.verify.as_table
255
+ #┌────────┬────────┬───────────┬──────────┬──────────┬────────────┬───────────────┬───────┬────────┬──────────┐
256
+ #│ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │
257
+ #╞════════╪════════╪═══════════╪══════════╪══════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡
258
+ #│ Future │ ES │ 428520022 │ GLOBEX │ 20210917 │ 50 │ ES │ │ │ USD │
259
+ #│ Future │ ES │ 446091461 │ GLOBEX │ 20211217 │ 50 │ ES │ │ │ USD │
260
+ #│ Future │ ES │ 461318816 │ GLOBEX │ 20220318 │ 50 │ ES │ │ │ USD │
261
+ #│ Future │ ES │ 477836957 │ GLOBEX │ 20220617 │ 50 │ ES │ │ │ USD │
262
+ #│ Future │ ES │ 495512551 │ GLOBEX │ 20221216 │ 50 │ ES │ │ │ USD │
263
+ #│ Future │ ES │ 495512552 │ GLOBEX │ 20231215 │ 50 │ ES │ │ │ USD │
264
+ #│ Future │ ES │ 495512557 │ GLOBEX │ 20241220 │ 50 │ ES │ │ │ USD │
265
+ #│ Future │ ES │ 495512563 │ GLOBEX │ 20251219 │ 50 │ ES │ │ │ USD │
266
+ #│ Future │ ES │ 495512566 │ GLOBEX │ 20220916 │ 50 │ ES │ │ │ USD │
267
+ #│ Future │ ES │ 495512569 │ GLOBEX │ 20230616 │ 50 │ ES │ │ │ USD │
268
+ #│ Future │ ES │ 495512572 │ GLOBEX │ 20230317 │ 50 │ ES │ │ │ USD │
269
+ #│ Future │ ES │ 497222760 │ GLOBEX │ 20230915 │ 50 │ ES │ │ │ USD │
270
+ #└────────┴────────┴───────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘
271
+
272
+ def merge **new_attributes
273
+
274
+ resetted_attributes = [:con_id, :local_symbol, :contract_detail]
275
+ ## last_trading_day / expiry needs special treatment
276
+ resetted_attributes << :last_trading_day if new_attributes.keys.include? :expiry
277
+ self.class.new attributes.reject{|k,_| resetted_attributes.include? k}.merge(new_attributes)
242
278
  end
243
279
 
244
280
  # Contract comparison
@@ -376,6 +412,29 @@ In places where these terms are used to indicate a concept, we have left them as
376
412
  Hash.new
377
413
  end
378
414
 
415
+
416
+ def table_header( &b )
417
+ if block_given?
418
+ [ yield(self) , 'symbol', 'con_id', 'exchange', 'expiry','multiplier', 'trading-class' , 'right', 'strike', 'currency' ]
419
+ else
420
+ [ '', 'symbol', 'con_id', 'exchange', 'expiry','multiplier', 'trading-class' , 'right', 'strike', 'currency' ]
421
+ end
422
+ end
423
+
424
+ def table_row
425
+ [ self.class.to_s.demodulize, symbol,
426
+ { value: con_id.zero? ? '' : con_id , alignment: :right},
427
+ { value: exchange, alignment: :center},
428
+ expiry,
429
+ { value: multiplier.zero?? "" : multiplier, alignment: :center},
430
+ { value: trading_class, alignment: :center},
431
+ {value: right == :none ? "": right, alignment: :center },
432
+ { value: strike.zero? ? "": strike, alignment: :right},
433
+ { value: currency, alignment: :center} ]
434
+
435
+ end
436
+
437
+
379
438
  end # class Contract
380
439
 
381
440
 
@@ -4,7 +4,7 @@ module IB
4
4
  validates_format_of :sec_type, :with => /\Aind\z/,
5
5
  :message => "should be a Index"
6
6
  def default_attributes
7
- super.merge :sec_type => :ind
7
+ super.merge :sec_type => 'IND'
8
8
  end
9
9
  def to_human
10
10
  "<Index: " + [symbol, currency].join(" ") + " (#{description}) >"
@@ -67,5 +67,18 @@ module IB
67
67
 
68
68
  end
69
69
 
70
+ def table_header
71
+ [ 'Greeks', 'price', 'impl. vola', 'dividend', 'delta','gamma', 'vega' , 'theta']
72
+ end
73
+
74
+ def table_row
75
+ outstr= ->( item ) { { value: item.nil? ? "--" : sprintf("%g" , item) , alignment: :right } }
76
+ outprice= ->( item ) { { value: item.nil? ? "--" : sprintf("%7.2f" , item) , alignment: :right } }
77
+ option_short = ->{"#{option.right} #{option.symbol}#{ "/"+ option.trading_class unless option.trading_class == option.symbol } #{option.expiry} #{option.strike}"}
78
+ [ option_short[], outprice[ option_price ], outprice[ implied_volatility ],
79
+ outprice[ pv_dividend ],
80
+ outprice[ delta ], outprice[ gamma ], outprice[ vega ] , outprice[ theta ] ]
81
+ end
82
+
70
83
  end # class
71
84
  end # module