ib-extensions 1.1 → 1.3

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.
@@ -11,7 +11,7 @@ The order is identified by local_id and perm_id
11
11
  Everything is carried out in a mutex-synchonized environment
12
12
  =end
13
13
  def update_order_dependent_object order_dependent_object # :nodoc:
14
- account_data do | a |
14
+ account_data do | a |
15
15
  order = if order_dependent_object.local_id.present?
16
16
  a.locate_order( :local_id => order_dependent_object.local_id)
17
17
  else
@@ -21,7 +21,7 @@ Everything is carried out in a mutex-synchonized environment
21
21
  end
22
22
  end
23
23
  def initialize_order_handling
24
- tws.subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg|
24
+ tws.subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg|
25
25
  case msg
26
26
 
27
27
  when IB::Messages::Incoming::CommissionReport
@@ -39,7 +39,6 @@ Everything is carried out in a mutex-synchonized environment
39
39
  logger.info { "Order State not assigned-- #{msg.order_state.to_human} ----------" } if success.nil?
40
40
 
41
41
  when IB::Messages::Incoming::OpenOrder
42
- ## todo --> handling of bags --> no con_id
43
42
  account_data(msg.order.account) do | this_account |
44
43
  # first update the contracts
45
44
  # make open order equal to IB::Spreads (include negativ con_id)
@@ -63,9 +62,9 @@ Everything is carried out in a mutex-synchonized environment
63
62
  o.executions << msg.execution
64
63
  if msg.execution.cumulative_quantity.to_i == o.total_quantity.abs
65
64
  logger.info{ "#{o.account} --> #{o.contract.symbol}: Execution completed" }
66
- o.order_states.first_or_create( IB::OrderState.new( perm_id: o.perm_id, local_id: o.local_id,
67
-
68
- status: 'Filled' ), :status )
65
+ o.order_states.first_or_create( IB::OrderState.new( perm_id: o.perm_id,
66
+ local_id: o.local_id,
67
+ status: 'Filled' ), :status )
69
68
  # update portfoliovalue
70
69
  a = @accounts.detect{ | x | x.account == o.account } # we are in a mutex controlled environment
71
70
  pv = a.portfolio_values.detect{ | y | y.contract.con_id == o.contract.con_id}
@@ -86,23 +85,38 @@ Everything is carried out in a mutex-synchonized environment
86
85
  end # do
87
86
  end # def subscribe
88
87
 
89
- # Resets the order-array for each account first.
88
+ # Resets the order-array for each account.
90
89
  # Requests all open (eg. pending) orders from the tws
91
90
  #
92
- # Waits until the OpenOrderEnd-Message is recieved
91
+ # Waits until the OpenOrderEnd-Message is received
93
92
 
94
93
 
95
94
  def request_open_orders
96
95
 
97
- exit_condition = false
98
- subscription = tws.subscribe( :OpenOrderEnd ) { exit_condition = true }
99
- account_data{| account | account.orders = [] }
96
+ q = Queue.new
97
+ subscription = tws.subscribe( :OpenOrderEnd ) { q.push(true) } # signal succsess
98
+ account_data {| account | account.orders = [] }
100
99
  send_message :RequestAllOpenOrders
101
- i=0; loop{ i+=1; sleep 0.01; break if i > 1000 || exit_condition }
100
+ ## the OpenOrderEnd-message usually appears after 0.1 sec.
101
+ ## we wait for 1 sec.
102
+ th = Thread.new{ sleep 1 ; q.close }
103
+
104
+ q.pop # wait for OpenOrderEnd or finishing of thread
105
+
102
106
  tws.unsubscribe subscription
107
+ if q.closed?
108
+ 5.times do
109
+ logger.fatal { "Is the API in read-only modus? No Open Order Message received! "}
110
+ sleep 0.2
111
+ end
112
+ else
113
+ Thread.kill(th)
114
+ q.close
115
+ account_data {| account | account.orders } # reset order array
116
+ end
103
117
  end
104
118
 
105
- alias update_orders request_open_orders
119
+ alias update_orders request_open_orders
106
120
 
107
121
 
108
122
 
@@ -116,17 +130,35 @@ end # module
116
130
  module IB
117
131
 
118
132
  class Order
133
+ # Auto Adjust implements a simple algotithm to ensure that an order is accepted
134
+ #
135
+ # It reads `contract_detail.min_tick`.
136
+ #
137
+ # If min_tick < 0.01, the real tick-increments differ fron the min_tick_value
138
+ #
139
+ # For J36 (jardines) min tick is 0.001, but the minimal increment is 0.005
140
+ # For Tui1 its the samme, min_tick is 0.00001 , minimal increment ist 0.00005
141
+ #
142
+ # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit.
143
+ #
144
+ # | min-tick | round |
145
+ # |--------------|------------|
146
+ # | 10 | 110 |
147
+ # | 1 | 111 |
148
+ # | 0.1 | 111.1 |
149
+ # | 0.001 | 111.11 |
150
+ # | 0.0001 | 111.11 |
151
+ # | 0.00001 | 111.111 |
152
+ # |--------------|------------|
153
+ #
119
154
  def auto_adjust
120
155
  # lambda to perform the calculation
121
156
  adjust_price = ->(a,b) do
122
- a = BigDecimal( a, 5 )
123
- b = BigDecimal( b, 5 )
124
- _,o = a.divmod(b)
125
- a - o
157
+ count = -Math.log10(b).round.to_i
158
+ count = count -1 if count > 2
159
+ a.round count
160
+
126
161
  end
127
- # adjust_price[2.6896, 0.1].to_f => 2.6
128
- # adjust_price[2.0896, 0.05].to_f => 2.05
129
- # adjust_price[2.0896, 0.002].to_f => 2.088
130
162
 
131
163
 
132
164
  error "No Contract provided to Auto adjust" unless contract.is_a? IB::Contract
@@ -134,11 +166,11 @@ module IB
134
166
  unless contract.is_a? IB::Bag
135
167
  # ensure that contract_details are present
136
168
 
137
- the_details = contract.contract_detail.present? ? contract.contract_detail : contract.verify.first.contract_detail
138
- # there are two attributes to consider: limit_price and aux_price
139
- # limit_price + aux_price may be nil or an empty string. Then ".to_f.zero?" becomes true
140
- self.limit_price= adjust_price.call(limit_price.to_f, the_details.min_tick) unless limit_price.to_f.zero?
141
- self.aux_price= adjust_price.call(aux_price.to_f, the_details.min_tick) unless aux_price.to_f.zero?
169
+ min_tick = contract.verify.first.contract_detail.min_tick
170
+ # there are two attributes to consider: limit_price and aux_price
171
+ # limit_price + aux_price may be nil or an empty string. Then ".to_f.zero?" becomes true
172
+ self.limit_price= adjust_price.call(limit_price.to_f, min_tick) unless limit_price.to_f.zero?
173
+ self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero?
142
174
  end
143
175
  end
144
176
  end # class Order
data/lib/ib/gateway.rb CHANGED
@@ -113,7 +113,7 @@ IB::Gateway.new serial_array: true (, ...)
113
113
  @connection_parameter = { received: serial_array, port: port, host: host, connect: false, logger: logger, client_id: client_id }
114
114
 
115
115
  @account_lock = Mutex.new
116
- @watchlists = watchlists
116
+ @watchlists = watchlists.map{ |b| IB::Symbols.allocate_collection b }
117
117
  @gateway_parameter = { s_m_a: subscribe_managed_accounts,
118
118
  s_a: subscribe_alerts,
119
119
  s_o_m: subscribe_order_messages,
@@ -133,7 +133,7 @@ IB::Gateway.new serial_array: true (, ...)
133
133
  begin
134
134
  i+=1
135
135
  if connect(100) # tries to connect for about 2h
136
- get_account_data(watchlists: watchlists.map{|b| IB::Symbols.allocate_collection b}) if get_account_data
136
+ get_account_data()
137
137
  # request_open_orders() if request_open_orders || get_account_data
138
138
  else
139
139
  @accounts = [] # definitivley reset @accounts
@@ -156,6 +156,10 @@ IB::Gateway.new serial_array: true (, ...)
156
156
  def active_watchlists
157
157
  @watchlists
158
158
  end
159
+ def add_watchlist watchlist
160
+ new_watchlist = IB::Symbols.allocate_collection( watchlist )
161
+ @watchlists << new_watchlist unless @watchlists.include?( new_watchlist )
162
+ end
159
163
 
160
164
  def get_host
161
165
  "#{@connection_parameter[:host]}: #{@connection_parameter[:port] }"
@@ -196,28 +200,21 @@ Weiterhin meldet sich die Anwendung zur Auswertung von Messages der TWS an.
196
200
  return false
197
201
  end
198
202
  rescue Errno::EHOSTUNREACH => e
199
- logger.error 'Cannot connect to specified host'
200
- logger.error e
203
+ error "Cannot connect to specified host #{e}", :reader, true
201
204
  return false
202
205
  rescue SocketError => e
203
- logger.error 'Wrong Adress, connection not possible'
206
+ error 'Wrong Adress, connection not possible', :reader, true
204
207
  return false
208
+ rescue IB::Error => e
209
+ logger.info e
205
210
  end
206
211
 
207
- tws.start_reader
208
- # let NextValidId-Event appear
209
- (1..30).each do |r|
210
- break if tws.next_local_id.present?
211
- sleep 0.1
212
- if r == 30
213
- error "Connected, NextLocalId is not initialized. Repeat with another client_id"
214
- end
215
- end
216
212
  # initialize @accounts (incl. aliases)
217
213
  tws.send_message( :RequestFA, fa_data_type: 3) if fa?
218
214
  logger.debug { "Communications successfully established" }
219
215
  # update open orders
220
216
  request_open_orders if @gateway_parameter[:s_o_m] || @gateway_parameter[:g_a_d]
217
+ true # return gatway object
221
218
  end # def
222
219
 
223
220
 
@@ -227,7 +224,7 @@ Weiterhin meldet sich die Anwendung zur Auswertung von Messages der TWS an.
227
224
  def reconnect
228
225
  if tws.present?
229
226
  disconnect
230
- sleep 1
227
+ sleep 0.1
231
228
  end
232
229
  logger.info "trying to reconnect ..."
233
230
  connect
@@ -312,13 +309,28 @@ account_data provides a thread-safe access to linked content of accounts
312
309
  It returns an Array of the return-values of the block
313
310
 
314
311
  If called without a parameter, all clients are accessed
312
+
313
+ Example
314
+
315
+ ```
316
+ g = IB::Gateway.current
317
+ # thread safe access
318
+ g.account_data &:portfolio_values
319
+
320
+ g.account_data &:account_values
321
+
322
+ # primitive access
323
+ g.clients.map &:portfolio_values
324
+ g.clients.map &:account_values
325
+
326
+ ```
315
327
  =end
316
328
 
317
329
  def account_data account_or_id=nil
318
330
 
319
331
  safe = ->(account) do
320
332
  @account_lock.synchronize do
321
- yield account
333
+ yield account
322
334
  end
323
335
  end
324
336
 
@@ -349,10 +361,9 @@ If called without a parameter, all clients are accessed
349
361
  ## a possible response is best defined before the connect-attempt is done
350
362
  # ## Attention
351
363
  # ## @accounts are not initialized yet (empty array)
352
- if block_given?
353
- yield self
364
+ yield self if block_given?
365
+
354
366
 
355
- end
356
367
  end
357
368
 
358
369
  =begin
@@ -364,7 +375,7 @@ Its always active.
364
375
  def initialize_managed_accounts
365
376
  rec_id = tws.subscribe( :ReceiveFA ) do |msg|
366
377
  msg.accounts.each do |a|
367
- account_data( a.account ){| the_account | the_account.update_attribute :alias, a.alias } unless a.alias.blank?
378
+ account_data( a.account ){| the_account | the_account.update_attribute :alias, a.alias } unless a.alias.blank?
368
379
  end
369
380
  logger.info { "Accounts initialized \n #{@accounts.map( &:to_human ).join " \n " }" }
370
381
  end
@@ -407,25 +418,28 @@ Its always active.
407
418
  # => 0.00066005
408
419
  #
409
420
  def check_connection
410
- answer = nil; count=0
411
- z= tws.subscribe( :CurrentTime ) { answer = true }
412
- while (answer.nil?)
421
+ q = Queue.new
422
+ count = 0
423
+ result = nil
424
+ z= tws.subscribe( :CurrentTime ) { q.push true }
425
+ loop do
413
426
  begin
414
427
  tws.send_message(:RequestCurrentTime) # 10 ms ##
415
- i=0; loop{ break if answer || i > 40; i+=1; sleep 0.0001}
428
+ th = Thread.new{ sleep 1 ; q.push nil }
429
+ result = q.pop
430
+ count+=1
431
+ break if result || count > 10
416
432
  rescue IOError, Errno::ECONNREFUSED # connection lost
417
- count = 6
433
+ count +=1
434
+ retry
418
435
  rescue IB::Error # not connected
419
436
  reconnect
420
- count +=1
421
- sleep 1
422
- retry if count <= 5
437
+ count = 0
438
+ retry
423
439
  end
424
- count +=1
425
- break if count > 5
426
440
  end
427
441
  tws.unsubscribe z
428
- count < 5 && answer # return value
442
+ result # return value
429
443
  end
430
444
  private
431
445
 
@@ -21,114 +21,125 @@ module IB
21
21
  #
22
22
  # If last_price is received, its returned.
23
23
  # If not, midpoint (bid+ask/2) is used. Else the closing price will be returned.
24
- #
24
+ #
25
25
  # Any value (even 0.0) which is stored in IB::Contract.misc indicates that the contract is
26
26
  # accepted by `place_order`.
27
- #
28
- # The result can be customized by a provided block.
29
- #
30
- # IB::Symbols::Stocks.sie.market_price{ |x| x }
31
- # -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3}
32
- #
33
- #
34
- # Raw-data are stored in the _bars_-attribute of IB::Contract
35
- # (volatile, ie. data are not preserved when the Object is copied)
36
- #
37
- #Example: IB::Stock.new(symbol: :ge).market_price
38
- # returns the current market-price
39
27
  #
40
- #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join
41
- # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message
42
- # and returns this value, too
43
- #
44
- #Raises IB::Error
45
- # if no Marketdata Subscription is present and delayed: false is specified
46
- #
47
- #
48
- # Solutions: Catch the Error and retry with delayed: true
49
- #
50
- # if that fails use alternative exchanges (look to Contract.valid_exchanges)
28
+ # The result can be customized by a provided block.
51
29
  #
52
- def market_price delayed: true, thread: false
30
+ # IB::Symbols::Stocks.sie.market_price{ |x| x }
31
+ # -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3}
32
+ #
33
+ #
34
+ # Raw-data are stored in the _bars_-attribute of IB::Contract
35
+ # (volatile, ie. data are not preserved when the Object is copied)
36
+ #
37
+ #Example: IB::Stock.new(symbol: :ge).market_price
38
+ # returns the current market-price
39
+ #
40
+ #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join
41
+ # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message
42
+ # and returns this value, too
43
+ #
44
+
45
+ def market_price delayed: true, thread: false, no_error: false
53
46
 
54
- tws= Connection.current # get the initialized ib-ruby instance
55
- the_id , the_price = nil, nil
56
- tickdata = Hash.new
57
- # define requested tick-attributes
58
- last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ],
59
- [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]]
60
- request_data_type = delayed ? :frozen_delayed : :frozen
47
+ tws= Connection.current # get the initialized ib-ruby instance
48
+ the_id , the_price = nil, nil
49
+ tickdata = Hash.new
50
+ q = Queue.new
51
+ # define requested tick-attributes
52
+ last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ],
53
+ [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]]
54
+ request_data_type = delayed ? :frozen_delayed : :frozen
61
55
 
62
- tws.send_message :RequestMarketDataType, :market_data_type => IB::MARKET_DATA_TYPES.rassoc( request_data_type).first
56
+ # From the tws-documentation (https://interactivebrokers.github.io/tws-api/market_data_type.html)
57
+ # Beginning in TWS v970, a IBApi.EClient.reqMarketDataType callback of 1 will occur automatically
58
+ # after invoking reqMktData if the user has live data permissions for the instrument.
59
+ #
60
+ # so - even if "delayed" is specified, realtime-data are returned if RT-permissions are present
61
+ #
63
62
 
64
- #keep the method-call running until the request finished
65
- #and cancel subscriptions to the message handler
66
- # method returns the (running) thread
67
- th = Thread.new do
68
- finalize, raise_delay_alert = false, false
69
- s_id = tws.subscribe(:TickSnapshotEnd){|x| finalize = true if x.ticker_id == the_id }
63
+ # method returns the (running) thread
64
+ th = Thread.new do
65
+ # about 11 sec after the request, the TWS returns :TickSnapshotEnd if no ticks are transmitted
66
+ # we don't have to implement out own timeout-criteria
67
+ s_id = tws.subscribe(:TickSnapshotEnd){|x| q.push(true) if x.ticker_id == the_id }
68
+ a_id = tws.subscribe(:Alert){|x| q.push(x) if [200, 354, 10167, 10168].include?( x.code ) && x.error_id == the_id }
69
+ # TWS Error 354: Requested market data is not subscribed.
70
+ # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id }
70
71
 
71
- e_id = tws.subscribe(:Alert){|x| raise_delay_alert = true if x.code == 354 && x.error_id == the_id }
72
- # TWS Error 354: Requested market data is not subscribed.
73
- # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id }
74
-
75
- # subscribe to TickPrices
76
- sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg|
77
- [last,close,bid,ask].each do |x|
78
- tickdata[x] = msg.the_data[:price] if x.include?( IB::TICK_TYPES[ msg.the_data[:tick_type]])
79
- # fast exit condition
80
- finalize = true if tickdata.size >= 4 || ( tickdata[bid].present? && tickdata[ask].present? )
81
- end if msg.ticker_id == the_id
82
- end
83
- # initialize »the_id« that is used to identify the received tick messages
84
- # by firing the market data request
85
- the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
72
+ # subscribe to TickPrices
73
+ sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg|
74
+ [last,close,bid,ask].each do |x|
75
+ tickdata[x] = msg.the_data[:price] if x.include?( IB::TICK_TYPES[ msg.the_data[:tick_type]])
76
+ # fast exit condition
77
+ q.push(true) if tickdata.size >= 4
78
+ end if msg.ticker_id == the_id
79
+ end
80
+ # initialize »the_id« that is used to identify the received tick messages
81
+ # by firing the market data request
82
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
86
83
 
87
- # todo implement config-feature to set timeout in configuration (DRY-Feature)
88
- # Alternative zu Timeout
89
- # Thread.new do
90
- # i=0; loop{ i+=1; sleep 0.1; break if finalize || i > 1000 }
84
+ while !q.closed? do
85
+ result = q.pop
86
+ if result.is_a? IB::Messages::Incoming::Alert
87
+ tws.logger.debug result.message
88
+ case result.code
89
+ when 200
90
+ q.close
91
+ error "#{to_human} --> #{result.message}" unless no_error
92
+ when 354, # not subscribed to market data
93
+ 10167,
94
+ 10168
95
+ if delayed && !(result.message =~ /market data is not available/)
96
+ tws.logger.debug "#{to_human} --> requesting delayed data"
97
+ tws.send_message :RequestMarketDataType, :market_data_type => 3
98
+ self.misc = :delayed
99
+ sleep 0.1
100
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
101
+ else
102
+ q.close
103
+ tws.logger.error "#{to_human} --> No marketdata permissions" unless no_error
104
+ end
105
+ end
106
+ elsif result.present?
107
+ q.close
108
+ tz = -> (z){ z.map{|y| y.to_s.split('_')}.flatten.count_duplicates.max_by{|k,v| v}.first.to_sym}
109
+ data = tickdata.map{|x,y| [tz[x],y]}.to_h
110
+ valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) }
111
+ self.bars << data # store raw data in bars
112
+ the_price = if block_given?
113
+ yield data
114
+ # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3}
115
+ else # behavior if no block is provided
116
+ if valid_data[data[:last]]
117
+ data[:last]
118
+ elsif valid_data[data[:bid]]
119
+ (data[:bid]+data[:ask])/2
120
+ elsif data[:close].present?
121
+ data[:close]
122
+ else
123
+ nil
124
+ end
125
+ end
91
126
 
92
- i=0;
93
- loop{ i+=1; break if i > 1000 || finalize || raise_delay_alert; sleep 0.05 }
94
- tws.unsubscribe sub_id, s_id, e_id
95
- # reduce :close_price delayed_close to close a.s.o
96
- if raise_delay_alert && !delayed
97
- error "No Marketdata Subscription, use delayed data <-- #{to_human}"
98
- # elsif raise_snapshot_alert
99
- # error "No Snapshot Permissions, try alternative exchange <-- #{to_human}"
100
- elsif i <= 1000
101
- tz = -> (z){ z.map{|y| y.to_s.split('_')}.flatten.count_duplicates.max_by{|k,v| v}.first.to_sym}
102
- data = tickdata.map{|x,y| [tz[x],y]}.to_h
103
- valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) }
104
- self.bars << data # store raw data in bars
105
- the_price = if block_given?
106
- yield data
107
- # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3}
108
- else # behavior if no block is provided
109
- if valid_data[data[:last]]
110
- data[:last]
111
- elsif valid_data[data[:bid]]
112
- (data[:bid]+data[:ask])/2
113
- elsif data[:close].present?
114
- data[:close]
115
- else
116
- nil
117
- end
118
- end
119
- self.misc = the_price if thread # store internally if in thread modus
120
- else # i > 1000
121
- tws.logger.info{ "#{to_human} --> No Marketdata received " }
122
- end
127
+ self.misc = misc == :delayed ? { :delayed => the_price } : { realtime: the_price }
128
+ else
129
+ q.close
130
+ error "#{to_human} --> No Marketdata received "
131
+ end
132
+ end
123
133
 
124
- end
125
- if thread
126
- th # return thread
127
- else
128
- th.join
129
- the_price # return
130
- end
131
- end #
134
+ tws.unsubscribe sub_id, s_id, a_id
135
+ end
136
+ if thread
137
+ th # return thread
138
+ else
139
+ th.join
140
+ the_price # return
141
+ end
142
+ end #
132
143
 
133
144
  end
134
145
  end