ib-extensions 1.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,36 +21,36 @@ 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
- #
27
+ #
28
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
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
35
  # (volatile, ie. data are not preserved when the Object is copied)
36
- #
36
+ #
37
37
  #Example: IB::Stock.new(symbol: :ge).market_price
38
38
  # returns the current market-price
39
39
  #
40
- #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join
40
+ #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join
41
41
  # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message
42
42
  # and returns this value, too
43
43
  #
44
44
 
45
45
  def market_price delayed: true, thread: false, no_error: false
46
46
 
47
- tws= Connection.current # get the initialized ib-ruby instance
47
+ tws= Connection.current # get the initialized ib-ruby instance
48
48
  the_id , the_price = nil, nil
49
49
  tickdata = Hash.new
50
50
  q = Queue.new
51
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 ]]
52
+ last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ],
53
+ [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]]
54
54
  request_data_type = delayed ? :frozen_delayed : :frozen
55
55
 
56
56
  # From the tws-documentation (https://interactivebrokers.github.io/tws-api/market_data_type.html)
@@ -62,42 +62,42 @@ module IB
62
62
 
63
63
  # method returns the (running) thread
64
64
  th = Thread.new do
65
- # about 11 sec after the request, the TES returns :TickSnapshotEnd if no ticks are transmitted
66
- # we don't have to implement out ow timeout-criteria
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
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 }
68
+ a_id = tws.subscribe(:Alert){|x| q.push(x) if [200, 354, 10167, 10168].include?( x.code ) && x.error_id == the_id }
69
69
  # TWS Error 354: Requested market data is not subscribed.
70
70
  # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id }
71
71
 
72
72
  # subscribe to TickPrices
73
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]])
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
76
  # fast exit condition
77
- q.push(true) if tickdata.size >= 4
78
- end if msg.ticker_id == the_id
77
+ q.push(true) if tickdata.size >= 4
78
+ end if msg.ticker_id == the_id
79
79
  end
80
80
  # initialize »the_id« that is used to identify the received tick messages
81
81
  # by firing the market data request
82
- the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
82
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
83
83
 
84
84
  while !q.closed? do
85
85
  result = q.pop
86
86
  if result.is_a? IB::Messages::Incoming::Alert
87
- tws.logger.info result.message
87
+ tws.logger.debug result.message
88
88
  case result.code
89
- when 200
89
+ when 200
90
90
  q.close
91
91
  error "#{to_human} --> #{result.message}" unless no_error
92
92
  when 354, # not subscribed to market data
93
93
  10167,
94
94
  10168
95
- if delayed
96
- tws.logger.info "#{to_human} --> requesting delayed data"
97
- tws.send_message :RequestMarketDataType, :market_data_type => 3
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
98
  self.misc = :delayed
99
99
  sleep 0.1
100
- the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
100
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
101
101
  else
102
102
  q.close
103
103
  tws.logger.error "#{to_human} --> No marketdata permissions" unless no_error
@@ -109,25 +109,25 @@ module IB
109
109
  data = tickdata.map{|x,y| [tz[x],y]}.to_h
110
110
  valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) }
111
111
  self.bars << data # store raw data in bars
112
- the_price = if block_given?
113
- yield data
112
+ the_price = if block_given?
113
+ yield data
114
114
  # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3}
115
115
  else # behavior if no block is provided
116
116
  if valid_data[data[:last]]
117
- data[:last]
117
+ data[:last]
118
118
  elsif valid_data[data[:bid]]
119
119
  (data[:bid]+data[:ask])/2
120
- elsif data[:close].present?
120
+ elsif data[:close].present?
121
121
  data[:close]
122
122
  else
123
123
  nil
124
124
  end
125
125
  end
126
126
 
127
- self.misc = misc == :delayed ? { :delayed => the_price } : { realtime: the_price }
127
+ self.misc = misc == :delayed ? { :delayed => the_price } : { realtime: the_price }
128
128
  else
129
129
  q.close
130
- error "#{to_human} --> No Marketdata received "
130
+ error "#{to_human} --> No Marketdata received "
131
131
  end
132
132
  end
133
133
 
@@ -137,7 +137,7 @@ module IB
137
137
  th # return thread
138
138
  else
139
139
  th.join
140
- the_price # return
140
+ the_price # return
141
141
  end
142
142
  end #
143
143