ib-api 10.33.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.
- checksums.yaml +7 -0
- data/.gitignore +52 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CLAUDE.md +131 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +120 -0
- data/Guardfile +24 -0
- data/LICENSE +674 -0
- data/LLM_GUIDE.md +388 -0
- data/README.md +114 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/api.gemspec +50 -0
- data/bin/console +96 -0
- data/bin/console.yml +3 -0
- data/bin/setup +8 -0
- data/bin/simple +91 -0
- data/changelog.md +32 -0
- data/conditions/ib/execution_condition.rb +31 -0
- data/conditions/ib/margin_condition.rb +28 -0
- data/conditions/ib/order_condition.rb +29 -0
- data/conditions/ib/percent_change_condition.rb +34 -0
- data/conditions/ib/price_condition.rb +44 -0
- data/conditions/ib/time_condition.rb +42 -0
- data/conditions/ib/volume_condition.rb +36 -0
- data/lib/class_extensions.rb +167 -0
- data/lib/ib/base.rb +109 -0
- data/lib/ib/base_properties.rb +178 -0
- data/lib/ib/connection.rb +573 -0
- data/lib/ib/constants.rb +402 -0
- data/lib/ib/contract.rb +30 -0
- data/lib/ib/errors.rb +52 -0
- data/lib/ib/messages/abstract_message.rb +68 -0
- data/lib/ib/messages/incoming/abstract_message.rb +116 -0
- data/lib/ib/messages/incoming/abstract_tick.rb +25 -0
- data/lib/ib/messages/incoming/account_message.rb +26 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +105 -0
- data/lib/ib/messages/incoming/contract_message.rb +13 -0
- data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
- data/lib/ib/messages/incoming/execution_data.rb +50 -0
- data/lib/ib/messages/incoming/histogram_data.rb +30 -0
- data/lib/ib/messages/incoming/historical_data.rb +65 -0
- data/lib/ib/messages/incoming/historical_data_update.rb +50 -0
- data/lib/ib/messages/incoming/managed_accounts.rb +21 -0
- data/lib/ib/messages/incoming/market_depth.rb +34 -0
- data/lib/ib/messages/incoming/market_depth_l2.rb +15 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +19 -0
- data/lib/ib/messages/incoming/open_order.rb +290 -0
- data/lib/ib/messages/incoming/order_status.rb +85 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +47 -0
- data/lib/ib/messages/incoming/position_data.rb +21 -0
- data/lib/ib/messages/incoming/positions_multi.rb +15 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/receive_fa.rb +30 -0
- data/lib/ib/messages/incoming/scanner_data.rb +54 -0
- data/lib/ib/messages/incoming/tick_by_tick.rb +77 -0
- data/lib/ib/messages/incoming/tick_efp.rb +18 -0
- data/lib/ib/messages/incoming/tick_generic.rb +12 -0
- data/lib/ib/messages/incoming/tick_option.rb +60 -0
- data/lib/ib/messages/incoming/tick_price.rb +60 -0
- data/lib/ib/messages/incoming/tick_size.rb +55 -0
- data/lib/ib/messages/incoming/tick_string.rb +13 -0
- data/lib/ib/messages/incoming.rb +292 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +84 -0
- data/lib/ib/messages/outgoing/bar_request_message.rb +247 -0
- data/lib/ib/messages/outgoing/new-place-order.rb +193 -0
- data/lib/ib/messages/outgoing/old-place-order.rb +147 -0
- data/lib/ib/messages/outgoing/place_order.rb +149 -0
- data/lib/ib/messages/outgoing/request_account_summary.rb +79 -0
- data/lib/ib/messages/outgoing/request_historical_data.rb +182 -0
- data/lib/ib/messages/outgoing/request_market_data.rb +102 -0
- data/lib/ib/messages/outgoing/request_market_depth.rb +57 -0
- data/lib/ib/messages/outgoing/request_real_time_bars.rb +48 -0
- data/lib/ib/messages/outgoing/request_scanner_subscription.rb +73 -0
- data/lib/ib/messages/outgoing/request_tick_by_tick_data.rb +21 -0
- data/lib/ib/messages/outgoing.rb +410 -0
- data/lib/ib/messages.rb +139 -0
- data/lib/ib/order_condition.rb +26 -0
- data/lib/ib/plugins.rb +27 -0
- data/lib/ib/prepare_data.rb +61 -0
- data/lib/ib/raw_message_parser.rb +99 -0
- data/lib/ib/socket.rb +83 -0
- data/lib/ib/support.rb +236 -0
- data/lib/ib/version.rb +6 -0
- data/lib/ib-api.rb +44 -0
- data/lib/server_versions.rb +145 -0
- data/lib/support/array_function.rb +28 -0
- data/lib/support/logging.rb +45 -0
- data/models/ib/account.rb +72 -0
- data/models/ib/account_value.rb +33 -0
- data/models/ib/bag.rb +55 -0
- data/models/ib/bar.rb +31 -0
- data/models/ib/combo_leg.rb +127 -0
- data/models/ib/contract.rb +411 -0
- data/models/ib/contract_detail.rb +118 -0
- data/models/ib/execution.rb +67 -0
- data/models/ib/forex.rb +12 -0
- data/models/ib/future.rb +64 -0
- data/models/ib/index.rb +14 -0
- data/models/ib/option.rb +149 -0
- data/models/ib/option_detail.rb +84 -0
- data/models/ib/order.rb +720 -0
- data/models/ib/order_state.rb +155 -0
- data/models/ib/portfolio_value.rb +86 -0
- data/models/ib/spread.rb +176 -0
- data/models/ib/stock.rb +25 -0
- data/models/ib/underlying.rb +32 -0
- data/plugins/ib/advanced-account.rb +442 -0
- data/plugins/ib/alerts/base-alert.rb +125 -0
- data/plugins/ib/alerts/gateway-alerts.rb +15 -0
- data/plugins/ib/alerts/order-alerts.rb +73 -0
- data/plugins/ib/auto-adjust.rb +0 -0
- data/plugins/ib/connection-tools.rb +122 -0
- data/plugins/ib/eod.rb +326 -0
- data/plugins/ib/greeks.rb +102 -0
- data/plugins/ib/managed-accounts.rb +274 -0
- data/plugins/ib/market-price.rb +150 -0
- data/plugins/ib/option-chain.rb +167 -0
- data/plugins/ib/order-flow.rb +157 -0
- data/plugins/ib/order-prototypes/abstract.rb +67 -0
- data/plugins/ib/order-prototypes/adaptive.rb +40 -0
- data/plugins/ib/order-prototypes/all-in-one.rb +46 -0
- data/plugins/ib/order-prototypes/combo.rb +46 -0
- data/plugins/ib/order-prototypes/forex.rb +40 -0
- data/plugins/ib/order-prototypes/limit.rb +193 -0
- data/plugins/ib/order-prototypes/market.rb +116 -0
- data/plugins/ib/order-prototypes/pegged.rb +169 -0
- data/plugins/ib/order-prototypes/premarket.rb +31 -0
- data/plugins/ib/order-prototypes/stop.rb +202 -0
- data/plugins/ib/order-prototypes/volatility.rb +39 -0
- data/plugins/ib/order-prototypes.rb +118 -0
- data/plugins/ib/probability-of-expiring.rb +109 -0
- data/plugins/ib/process-orders.rb +155 -0
- data/plugins/ib/roll.rb +86 -0
- data/plugins/ib/spread-prototypes/butterfly.rb +77 -0
- data/plugins/ib/spread-prototypes/calendar.rb +97 -0
- data/plugins/ib/spread-prototypes/stock-spread.rb +56 -0
- data/plugins/ib/spread-prototypes/straddle.rb +70 -0
- data/plugins/ib/spread-prototypes/strangle.rb +93 -0
- data/plugins/ib/spread-prototypes/vertical.rb +83 -0
- data/plugins/ib/spread-prototypes.rb +70 -0
- data/plugins/ib/symbols/abstract.rb +136 -0
- data/plugins/ib/symbols/bonds.rb +28 -0
- data/plugins/ib/symbols/cfd.rb +19 -0
- data/plugins/ib/symbols/combo.rb +46 -0
- data/plugins/ib/symbols/commodity.rb +17 -0
- data/plugins/ib/symbols/forex.rb +41 -0
- data/plugins/ib/symbols/futures.rb +127 -0
- data/plugins/ib/symbols/index.rb +43 -0
- data/plugins/ib/symbols/options.rb +99 -0
- data/plugins/ib/symbols/stocks.rb +44 -0
- data/plugins/ib/symbols/version.rb +5 -0
- data/plugins/ib/symbols.rb +118 -0
- data/plugins/ib/verify.rb +226 -0
- data/symbols/w20.yml +210 -0
- data/t.txt +20 -0
- data/update.md +71 -0
- metadata +327 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
# Encapsulates API connection to TWS or Gateway
|
|
3
|
+
class Connection
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## -------------------------------------------- Interface ---------------------------------
|
|
7
|
+
## public attributes: socket, next_local_id ( alias next_order_id)
|
|
8
|
+
## public methods: connect (alias open), disconnect, connected?
|
|
9
|
+
## subscribe, unsubscribe
|
|
10
|
+
## send_message (alias dispatch)
|
|
11
|
+
## place_order, modify_order, cancel_order
|
|
12
|
+
## public data-queue: received, received?, wait_for, clear_received
|
|
13
|
+
## misc: reader_running?
|
|
14
|
+
|
|
15
|
+
include ::Support::Logging # provides default_logger
|
|
16
|
+
include Plugins
|
|
17
|
+
include Workflow
|
|
18
|
+
|
|
19
|
+
mattr_accessor :current
|
|
20
|
+
|
|
21
|
+
attr_accessor :socket # Socket to IB server (TWS or Gateway)
|
|
22
|
+
attr_accessor :next_local_id # Next valid order id
|
|
23
|
+
attr_accessor :client_id
|
|
24
|
+
attr_accessor :server_version
|
|
25
|
+
attr_accessor :client_version
|
|
26
|
+
attr_accessor :host
|
|
27
|
+
attr_accessor :received
|
|
28
|
+
attr_accessor :port
|
|
29
|
+
attr_accessor :plugins
|
|
30
|
+
alias next_order_id next_local_id
|
|
31
|
+
alias next_order_id= next_local_id=
|
|
32
|
+
|
|
33
|
+
def workflow_state
|
|
34
|
+
@workflow_state
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
workflow do
|
|
38
|
+
state :virgin do
|
|
39
|
+
event :try_connection, transitions_to: :ready
|
|
40
|
+
event :activate_managed_accounts, transitions_to: :gateway_mode
|
|
41
|
+
event :collect_data, transitions_to: :lean_mode
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
state :lean_mode do
|
|
45
|
+
event :try_connection, transitions_to: :ready
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
state :gateway_mode do
|
|
49
|
+
event :try_connection, transitions_to: :ready
|
|
50
|
+
event :initialize_managed_accounts, transitions_to: :account_based_operations
|
|
51
|
+
end
|
|
52
|
+
state :ready do
|
|
53
|
+
event :initialize_managed_accounts, transitions_to: :account_based_operations # plugin managed_account
|
|
54
|
+
event :disconnect, transitions_to: :disconnected
|
|
55
|
+
end
|
|
56
|
+
state :disconnected do
|
|
57
|
+
event :try_connection, transitions_to: :ready
|
|
58
|
+
event :activate_managed_accounts, transitions_to: :gateway_mode # plugin managed_account
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
state :account_based_operations do
|
|
63
|
+
event :disconnect, transitions_to: :disconnected
|
|
64
|
+
event :initialize_order_handling, transitions_to: :account_based_orderflow # plugin process-orders
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
state :account_based_orderflow do
|
|
68
|
+
event :disconnect, transitions_to: :disconnected
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
on_transition do |from, to, triggering_event, *event_args|
|
|
72
|
+
logger.warn{ "Workflow:: #{workflow_state} -> #{to}" }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def initialize host: '127.0.0.1:4002', # combination of host + port
|
|
78
|
+
port: nil,
|
|
79
|
+
#:port => '7497', # TWS connection --> demo 7496: production
|
|
80
|
+
# connect: true, # Connect at initialization ---> disabled in favour of Connection.new.try_connection!
|
|
81
|
+
# received: true, # Keep all received messages in a @received Hash ---> disabled; automatically disabled in lean- and
|
|
82
|
+
# gateway-modus
|
|
83
|
+
logger: nil,
|
|
84
|
+
client_id: rand( 1001 .. 9999 ) ,
|
|
85
|
+
client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
|
|
86
|
+
optional_capacities: "", # TWS-Version 974: "+PACEAPI"
|
|
87
|
+
plugins: [],
|
|
88
|
+
#server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
|
|
89
|
+
**any_other_parameters_which_are_ignored
|
|
90
|
+
# V 974 release motes
|
|
91
|
+
# 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.
|
|
92
|
+
|
|
93
|
+
Connection.current = self
|
|
94
|
+
self.class.configure_logger logger
|
|
95
|
+
# enable specification of host and port through host: 'localhost:4002' as parameter
|
|
96
|
+
host, port = (host+':'+port.to_s).split(':')
|
|
97
|
+
# convert parameters into instance-variables and assign them
|
|
98
|
+
method(__method__).parameters.each do |type, k|
|
|
99
|
+
next unless type == :key ## available: key , keyrest
|
|
100
|
+
next if k.to_s == 'logger'
|
|
101
|
+
v = eval(k.to_s)
|
|
102
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A couple of locks to avoid race conditions in JRuby
|
|
106
|
+
@subscribe_lock = Mutex.new
|
|
107
|
+
@receive_lock = Mutex.new
|
|
108
|
+
@message_lock = Mutex.new
|
|
109
|
+
|
|
110
|
+
@parser = nil
|
|
111
|
+
@connected = false
|
|
112
|
+
|
|
113
|
+
@plugins.each do |name|
|
|
114
|
+
activate_plugin name
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@next_local_id = nil
|
|
118
|
+
|
|
119
|
+
#
|
|
120
|
+
# this block is executed before tws-communication is established
|
|
121
|
+
# Its intended for globally available subscriptions of tws-messages
|
|
122
|
+
yield self if block_given?
|
|
123
|
+
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# read actual order_id and
|
|
127
|
+
# connect if not connected
|
|
128
|
+
def update_next_order_id
|
|
129
|
+
q = Queue.new
|
|
130
|
+
subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id }
|
|
131
|
+
try_connection! unless connected?
|
|
132
|
+
send_message :RequestIds
|
|
133
|
+
th = Thread.new { sleep 5; q.close }
|
|
134
|
+
@next_local_id = q.pop
|
|
135
|
+
if q.closed?
|
|
136
|
+
error "Could not get NextValidID", :reader
|
|
137
|
+
else
|
|
138
|
+
th.kill
|
|
139
|
+
end
|
|
140
|
+
unsubscribe subscription
|
|
141
|
+
@next_local_id # return next_id
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
### Working with connection
|
|
145
|
+
def connected?
|
|
146
|
+
@connected
|
|
147
|
+
end
|
|
148
|
+
#
|
|
149
|
+
### Event – call through Connection-object.try_connection!
|
|
150
|
+
protected
|
|
151
|
+
def try_connection
|
|
152
|
+
logger.progname='IB::Connection#Event:TryConnection'
|
|
153
|
+
if connected?
|
|
154
|
+
error "Already connected!"
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
# TWS always sends NextValidId message at connect -subscribe save this id
|
|
158
|
+
subscribe(:NextValidId) do |msg|
|
|
159
|
+
logger.progname = "Connection"
|
|
160
|
+
@next_local_id = msg.local_id
|
|
161
|
+
logger.info { "Got next valid order id: #{@next_local_id}." }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible
|
|
165
|
+
socket.initialising_handshake
|
|
166
|
+
@parser = RawMessageParser.new socket
|
|
167
|
+
@parser.each do | the_message |
|
|
168
|
+
# socket.decode_message( socket.receive_messages ) do | the_message |
|
|
169
|
+
#puts "TheMessage :: #{the_message.inspect}"
|
|
170
|
+
@server_version = the_message.shift.to_i.freeze
|
|
171
|
+
error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER
|
|
172
|
+
|
|
173
|
+
@remote_connect_time = DateTime.parse the_message.shift.freeze
|
|
174
|
+
@local_connect_time = Time.now.freeze
|
|
175
|
+
@connected = true
|
|
176
|
+
break # only receive one message
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# V100 initial handshake
|
|
180
|
+
# Parameters borrowed from the python client
|
|
181
|
+
start_api = 71
|
|
182
|
+
version = 2
|
|
183
|
+
# optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
|
|
184
|
+
socket.send_messages start_api, version, @client_id , @optional_capacities
|
|
185
|
+
logger.fatal{ "Connected to server, version: #{@server_version}, " +
|
|
186
|
+
"using client-id: #{client_id},\n connection time: " +
|
|
187
|
+
"#{@local_connect_time} local, " +
|
|
188
|
+
"#{@remote_connect_time} remote." }
|
|
189
|
+
start_reader
|
|
190
|
+
rescue IB::TransmissionError =>e
|
|
191
|
+
logger.fatal "Transmission Error: Retrying establishing the connection"
|
|
192
|
+
logger.fatal e.msg
|
|
193
|
+
disconnect!
|
|
194
|
+
try_connection!
|
|
195
|
+
# update_next_order_id
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
### Event – call through Connection-object.disconnect!
|
|
200
|
+
def disconnect
|
|
201
|
+
if reader_running?
|
|
202
|
+
@reader_running = false
|
|
203
|
+
@reader_thread.join
|
|
204
|
+
end
|
|
205
|
+
socket.close
|
|
206
|
+
@connected = false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
public
|
|
211
|
+
|
|
212
|
+
# disconnect and restart communication with the tws.
|
|
213
|
+
#
|
|
214
|
+
# cancels all subscriptions and reestablishes standard
|
|
215
|
+
# subscriptions for the current workflow state.
|
|
216
|
+
#
|
|
217
|
+
# connects if called in the disconnected state
|
|
218
|
+
#
|
|
219
|
+
#
|
|
220
|
+
# Usecase:
|
|
221
|
+
# 3.2.0 :015 > Symbols::Stocks.msft.verify
|
|
222
|
+
# A: Error reading request. Unable to parse data. java.lang.NumberFormatException: For input string: "MSFT"
|
|
223
|
+
#
|
|
224
|
+
# ^C/usr/share/rvm/rubies/ruby-3.2.0/lib/ruby/3.2.0/irb.rb:438:in `raise': abort then interrupt! (IRB::Abort)
|
|
225
|
+
# from <internal:thread_sync>:18:in `pop'
|
|
226
|
+
# from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:164:in `_verify'
|
|
227
|
+
# from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:78:in `verify'
|
|
228
|
+
# from (irb):15:in `<main>'
|
|
229
|
+
# from ./console:96:in `<main>'
|
|
230
|
+
# 3.2.0 :016 > C.reconnect
|
|
231
|
+
# F: Connected to server, version: 165, using client-id: 2000,
|
|
232
|
+
# connection time: 2024-11-21 20:57:57 +0100 local, 2024-11-21T19:57:57+00:00 remote.
|
|
233
|
+
# => 227896
|
|
234
|
+
# 3.2.0 :017 > Symbols::Stocks.msft.verify
|
|
235
|
+
# => [<Stock: MSFT USD NASDAQ>]
|
|
236
|
+
#
|
|
237
|
+
def reconnect
|
|
238
|
+
return if workflow_state == "virgin"
|
|
239
|
+
old_workflowstate = workflow_state.dup
|
|
240
|
+
disconnect! unless disconnected?
|
|
241
|
+
|
|
242
|
+
unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq
|
|
243
|
+
|
|
244
|
+
if ["ready", "lean_mode", "disconnected"].include? old_workflowstate
|
|
245
|
+
try_connection!
|
|
246
|
+
else
|
|
247
|
+
activate_managed_accounts!
|
|
248
|
+
unless old_workflowstate == 'gateway_mode'
|
|
249
|
+
initialize_managed_accounts!
|
|
250
|
+
initialize_order_handling! unless old_workflowstate != "account_based_orderflow"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
### Working with message subscribers
|
|
257
|
+
|
|
258
|
+
# Subscribe Proc or block to specific type(s) of incoming message events.
|
|
259
|
+
# Listener will be called later with received message instance as its argument.
|
|
260
|
+
# Returns subscriber id to allow unsubscribing
|
|
261
|
+
def subscribe *args, &block
|
|
262
|
+
@subscribe_lock.synchronize do
|
|
263
|
+
subscriber = args.last.respond_to?(:call) ? args.pop : block
|
|
264
|
+
id = random_id
|
|
265
|
+
|
|
266
|
+
error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc
|
|
267
|
+
|
|
268
|
+
args.each do |what|
|
|
269
|
+
message_classes =
|
|
270
|
+
case
|
|
271
|
+
when what.is_a?(Class) && what < IB::Messages::Incoming::AbstractMessage
|
|
272
|
+
[what]
|
|
273
|
+
when what.is_a?(Symbol)
|
|
274
|
+
if IB::Messages::Incoming.const_defined?(what)
|
|
275
|
+
[IB::Messages::Incoming.const_get(what)]
|
|
276
|
+
# elsif TechnicalAnalysis::Signals.const_defined?(what)
|
|
277
|
+
# [TechnicalAnalysis::Signals.const_get?(what)]
|
|
278
|
+
else
|
|
279
|
+
error "#{what} is no IB::Messages class"
|
|
280
|
+
end
|
|
281
|
+
when what.is_a?(Regexp)
|
|
282
|
+
Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
|
|
283
|
+
else
|
|
284
|
+
error "#{what} must represent incoming IB message class"
|
|
285
|
+
end
|
|
286
|
+
# @subscribers_lock.synchronize do
|
|
287
|
+
message_classes.flatten.each do |message_class|
|
|
288
|
+
# TODO: Fix: RuntimeError: can't add a new key into hash during iteration
|
|
289
|
+
subscribers[message_class][id] = subscriber
|
|
290
|
+
end
|
|
291
|
+
# end # lock
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
id
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Remove all subscribers with specific subscriber id
|
|
299
|
+
def unsubscribe *ids
|
|
300
|
+
@subscribe_lock.synchronize do
|
|
301
|
+
ids.collect do |id|
|
|
302
|
+
removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
|
|
303
|
+
logger.error "No subscribers with id #{id}" if removed_at_id.empty?
|
|
304
|
+
removed_at_id # return_value
|
|
305
|
+
end.flatten
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
### Working with received messages Hash
|
|
311
|
+
# Clear received messages Hash
|
|
312
|
+
def clear_received *message_types
|
|
313
|
+
@receive_lock.synchronize do
|
|
314
|
+
if message_types.empty?
|
|
315
|
+
received.each { |message_type, container| container.clear }
|
|
316
|
+
else
|
|
317
|
+
message_types.each { |message_type| received[message_type].clear }
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Hash of received messages, keyed by message type
|
|
325
|
+
def received
|
|
326
|
+
@received_hash ||= Hash.new do |hash, message_type|
|
|
327
|
+
# enable access to the hash via
|
|
328
|
+
# ib.received[:MessageType].attribute
|
|
329
|
+
the_array = Array.new
|
|
330
|
+
def the_array.method_missing(method, *key)
|
|
331
|
+
unless method == :to_hash || method == :to_str #|| method == :to_int
|
|
332
|
+
return self.map{|x| x.public_send(method, *key)}
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
hash[message_type] = the_array
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Check if messages of given type were received at_least n times
|
|
340
|
+
def received? message_type, times=1
|
|
341
|
+
@receive_lock.synchronize do
|
|
342
|
+
received[message_type].size >= times
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# Wait for specific condition(s) - given as callable/block, or
|
|
348
|
+
# message type(s) - given as Symbol or [Symbol, times] pair.
|
|
349
|
+
# Timeout after given time or 1 second.
|
|
350
|
+
# wait_for depends on Connection#received. If collection of messages through recieved
|
|
351
|
+
# is turned off, wait_for loses most of its functionality
|
|
352
|
+
|
|
353
|
+
def wait_for *args, &block
|
|
354
|
+
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
|
355
|
+
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
|
356
|
+
conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
|
|
357
|
+
|
|
358
|
+
until end_time < Time.now || satisfied?(*conditions)
|
|
359
|
+
if reader_running?
|
|
360
|
+
sleep 0.05
|
|
361
|
+
else
|
|
362
|
+
process_messages 50
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
### Working with Incoming messages from IB
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
protected
|
|
371
|
+
def reader_running?
|
|
372
|
+
@reader_running && @reader_thread && @reader_thread.alive?
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
|
376
|
+
def process_messages poll_time = 50 # in msec
|
|
377
|
+
time_out = Time.now + poll_time/1000.0
|
|
378
|
+
begin
|
|
379
|
+
while (time_left = time_out - Time.now) > 0
|
|
380
|
+
# If socket is readable, process single incoming message
|
|
381
|
+
# windows environment: just read the socket
|
|
382
|
+
if RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/)
|
|
383
|
+
process_message if select [socket], nil, nil, time_left
|
|
384
|
+
else
|
|
385
|
+
# the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
|
|
386
|
+
# unfortunately, it raises Errors in windows environment
|
|
387
|
+
if select [socket], nil, nil, time_left
|
|
388
|
+
# Peek at the message from the socket; if it's blank then the
|
|
389
|
+
# server side of connection (TWS) has likely shut down.
|
|
390
|
+
socket_likely_shutdown = socket.recvmsg(100, ::Socket::MSG_PEEK)[0] == ""
|
|
391
|
+
# We go ahead process messages regardless (a no-op if socket_likely_shutdown).
|
|
392
|
+
process_message
|
|
393
|
+
# After processing, if socket has shut down we sleep for 100ms
|
|
394
|
+
# to avoid spinning in a tight loop. If the server side somehow
|
|
395
|
+
# comes back up (gets reconnedted), normal processing
|
|
396
|
+
# (without the 100ms wait) should happen.
|
|
397
|
+
sleep(0.1) if socket_likely_shutdown
|
|
398
|
+
end # if
|
|
399
|
+
end # if
|
|
400
|
+
end # while
|
|
401
|
+
rescue Errno::ECONNRESET => e
|
|
402
|
+
logger.fatal e.message
|
|
403
|
+
if e.message =~ /Connection reset by peer/
|
|
404
|
+
logger.fatal "Is another client listening on the same port?"
|
|
405
|
+
error "try reconnecting with a different client-id", :reader
|
|
406
|
+
else
|
|
407
|
+
logger.fatal "Aborting"
|
|
408
|
+
Kernel.exit
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
### Sending Outgoing messages to IB
|
|
414
|
+
|
|
415
|
+
# Send an outgoing message.
|
|
416
|
+
# returns the used request_id if appropiate, otherwise "true"
|
|
417
|
+
public
|
|
418
|
+
def send_message what, *args
|
|
419
|
+
message =
|
|
420
|
+
case
|
|
421
|
+
when what.is_a?(Messages::Outgoing::AbstractMessage)
|
|
422
|
+
what
|
|
423
|
+
when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
|
|
424
|
+
what.new *args
|
|
425
|
+
when what.is_a?(Symbol)
|
|
426
|
+
Messages::Outgoing.const_get(what).new *args
|
|
427
|
+
else
|
|
428
|
+
error "Only able to send outgoing IB messages"
|
|
429
|
+
end
|
|
430
|
+
error "Not able to send messages, IB not connected!" unless connected?
|
|
431
|
+
begin
|
|
432
|
+
@message_lock.synchronize do
|
|
433
|
+
message.send_to socket
|
|
434
|
+
end
|
|
435
|
+
rescue Errno::EPIPE
|
|
436
|
+
logger.error{ "Broken Pipe, trying to reconnect" }
|
|
437
|
+
reconnect
|
|
438
|
+
retry
|
|
439
|
+
end
|
|
440
|
+
## return the transmitted message
|
|
441
|
+
message.data[:request_id].presence || true
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
alias dispatch send_message # Legacy alias
|
|
445
|
+
|
|
446
|
+
# Place Order (convenience wrapper for send_message :PlaceOrder).
|
|
447
|
+
# Assigns client_id and order_id fields to placed order. Returns assigned order_id.
|
|
448
|
+
def place_order order, contract
|
|
449
|
+
# order.place contract, self ## old
|
|
450
|
+
error "Unable to place order, next_local_id not known" unless @next_local_id
|
|
451
|
+
error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil?
|
|
452
|
+
order.client_id = client_id
|
|
453
|
+
order.local_id = @next_local_id
|
|
454
|
+
@next_local_id += 1
|
|
455
|
+
order.placed_at = Time.now
|
|
456
|
+
modify_order order, contract
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id.
|
|
460
|
+
def modify_order order, contract
|
|
461
|
+
error "Unable to modify order; local_id not specified" if order.local_id.nil?
|
|
462
|
+
order.modified_at = Time.now
|
|
463
|
+
# if con_id is present, to place an order use only con_id and exchange
|
|
464
|
+
send_message :PlaceOrder,
|
|
465
|
+
:order => order.then{|y| y.contract = nil; y},
|
|
466
|
+
:contract => contract.con_id.to_i > 0 ? Contract.new( con_id: contract.con_id, exchange: contract.exchange || 'SMART' ) : contract,
|
|
467
|
+
:local_id => order.local_id
|
|
468
|
+
order.local_id # return value
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Cancel Orders by their local ids (convenience wrapper for send_message :CancelOrder).
|
|
472
|
+
def cancel_order *local_ids
|
|
473
|
+
local_ids.each do |local_id|
|
|
474
|
+
send_message :CancelOrder, :local_id => local_id.to_i
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Start reader thread that continuously reads messages from @socket in background.
|
|
479
|
+
# If you don't start reader, you should manually poll @socket for messages
|
|
480
|
+
# or use #process_messages(msec) API.
|
|
481
|
+
protected
|
|
482
|
+
def start_reader
|
|
483
|
+
if @reader_running
|
|
484
|
+
@reader_thread
|
|
485
|
+
else # connected? # if called from try_connection, the connected state is not set
|
|
486
|
+
begin
|
|
487
|
+
Thread.abort_on_exception = true
|
|
488
|
+
@reader_running = true
|
|
489
|
+
@reader_thread = Thread.new { process_messages while @reader_running }
|
|
490
|
+
rescue Errno::ECONNRESET => e
|
|
491
|
+
logger.fatal e.message
|
|
492
|
+
reconnect
|
|
493
|
+
end
|
|
494
|
+
# else
|
|
495
|
+
# error "Could not start reader, not connected!", :reader, true
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Message subscribers. Key is the message class to listen for.
|
|
500
|
+
# Value is a Hash of subscriber Procs, keyed by their subscription id.
|
|
501
|
+
# All subscriber Procs will be called with the message instance
|
|
502
|
+
# as an argument when a message of that type is received.
|
|
503
|
+
def subscribers
|
|
504
|
+
@subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Process single incoming message (blocking!)
|
|
508
|
+
def process_message
|
|
509
|
+
logger.progname='IB::Connection#process_message'
|
|
510
|
+
|
|
511
|
+
## decode mesage is included throught `prepare_data
|
|
512
|
+
# socket.decode_message( socket.receive_messages ) do | the_decoded_message |
|
|
513
|
+
@parser.each do | the_decoded_message |
|
|
514
|
+
# puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}"
|
|
515
|
+
msg_id = the_decoded_message.shift.to_i
|
|
516
|
+
|
|
517
|
+
# Debug:
|
|
518
|
+
# logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"}
|
|
519
|
+
|
|
520
|
+
# Create new instance of the appropriate message type,
|
|
521
|
+
# and have it read the message from socket.
|
|
522
|
+
# NB: Failure here usually means unsupported message type received
|
|
523
|
+
|
|
524
|
+
## raising IB::TransmissionError if something went wrong.
|
|
525
|
+
## the calling program has to initiate reconnection
|
|
526
|
+
logger.fatal { the_decoded_message } unless Messages::Incoming::Classes[msg_id]
|
|
527
|
+
error "Got unsupported message #{msg_id}", :reader unless Messages::Incoming::Classes[msg_id]
|
|
528
|
+
error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero?
|
|
529
|
+
begin
|
|
530
|
+
msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
|
|
531
|
+
rescue IB::TransmissionError
|
|
532
|
+
logger.fatal { the_decoded_message }
|
|
533
|
+
raise
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Deliver message to all registered subscribers, alert if no subscribers
|
|
537
|
+
# Ruby 2.0 and above: Hashes are ordered.
|
|
538
|
+
# Thus first declared subscribers of a class are executed first
|
|
539
|
+
@subscribe_lock.synchronize do
|
|
540
|
+
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
|
541
|
+
end
|
|
542
|
+
logger.info { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty?
|
|
543
|
+
|
|
544
|
+
# Collect all received messages into a @received Hash
|
|
545
|
+
if @received
|
|
546
|
+
@receive_lock.synchronize do
|
|
547
|
+
received[msg.message_type] << msg
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def random_id
|
|
554
|
+
rand 999999
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Check if all given conditions are satisfied
|
|
558
|
+
def satisfied? *conditions
|
|
559
|
+
!conditions.empty? &&
|
|
560
|
+
conditions.inject(true) do |result, condition|
|
|
561
|
+
result && if condition.is_a?(Symbol)
|
|
562
|
+
received?(condition)
|
|
563
|
+
elsif condition.is_a?(Array)
|
|
564
|
+
received?(*condition)
|
|
565
|
+
elsif condition.respond_to?(:call)
|
|
566
|
+
condition.call
|
|
567
|
+
else
|
|
568
|
+
logger.error { "Unknown wait condition #{condition}" }
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end # class Connection
|
|
573
|
+
end # module IB
|