ib-extensions 1.2 → 1.3.1

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,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