ib-ruby 0.4.3 → 0.4.20
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/.gitignore +32 -0
- data/HISTORY +68 -0
- data/README.rdoc +9 -6
- data/VERSION +1 -1
- data/bin/account_info +29 -0
- data/bin/contract_details +37 -0
- data/bin/depth_of_market +43 -0
- data/bin/historic_data +62 -0
- data/bin/{RequestHistoricData → historic_data_cli} +46 -91
- data/bin/market_data +49 -0
- data/bin/option_data +45 -0
- data/bin/template +21 -0
- data/bin/time_and_sales +63 -0
- data/lib/ib-ruby/connection.rb +166 -0
- data/lib/ib-ruby/constants.rb +91 -0
- data/lib/ib-ruby/messages/incoming.rb +807 -0
- data/lib/ib-ruby/messages/outgoing.rb +573 -0
- data/lib/ib-ruby/messages.rb +8 -1445
- data/lib/ib-ruby/models/bar.rb +26 -0
- data/lib/ib-ruby/models/contract.rb +335 -0
- data/lib/ib-ruby/models/execution.rb +55 -0
- data/lib/ib-ruby/models/model.rb +20 -0
- data/lib/ib-ruby/models/order.rb +262 -0
- data/lib/ib-ruby/models.rb +11 -0
- data/lib/ib-ruby/socket.rb +50 -0
- data/lib/ib-ruby/symbols/forex.rb +32 -72
- data/lib/ib-ruby/symbols/futures.rb +47 -68
- data/lib/ib-ruby/symbols/options.rb +30 -0
- data/lib/ib-ruby/symbols/stocks.rb +23 -0
- data/lib/ib-ruby/symbols.rb +9 -0
- data/lib/ib-ruby.rb +7 -8
- data/lib/legacy/bin/account_info_old +36 -0
- data/lib/legacy/bin/historic_data_old +81 -0
- data/lib/legacy/bin/market_data_old +68 -0
- data/lib/legacy/datatypes.rb +485 -0
- data/lib/legacy/ib-ruby.rb +10 -0
- data/lib/legacy/ib.rb +226 -0
- data/lib/legacy/messages.rb +1458 -0
- data/lib/version.rb +2 -3
- data/spec/ib-ruby/models/contract_spec.rb +261 -0
- data/spec/ib-ruby/models/order_spec.rb +64 -0
- data/spec/ib-ruby_spec.rb +0 -131
- metadata +106 -76
- data/bin/AccountInfo +0 -67
- data/bin/HistoricToCSV +0 -111
- data/bin/RequestMarketData +0 -78
- data/bin/SimpleTimeAndSales +0 -98
- data/bin/ib-ruby +0 -8
- data/lib/ib-ruby/datatypes.rb +0 -400
- data/lib/ib-ruby/ib.rb +0 -242
data/lib/legacy/ib.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'logger'
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'bigdecimal/util'
|
5
|
+
|
6
|
+
if RUBY_VERSION < "1.9"
|
7
|
+
require 'sha1'
|
8
|
+
else
|
9
|
+
require 'digest/sha1'
|
10
|
+
include Digest
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add method to_ib to render datetime in IB format (zero padded "yyyymmdd HH:mm:ss")
|
14
|
+
class Time
|
15
|
+
def to_ib
|
16
|
+
"#{self.year}#{sprintf("%02d", self.month)}#{sprintf("%02d", self.day)} " +
|
17
|
+
"#{sprintf("%02d", self.hour)}:#{sprintf("%02d", self.min)}:#{sprintf("%02d", self.sec)}"
|
18
|
+
end
|
19
|
+
end # Time
|
20
|
+
|
21
|
+
|
22
|
+
module IB
|
23
|
+
|
24
|
+
#logger = Logger.new(STDERR)
|
25
|
+
|
26
|
+
class IBSocket < TCPSocket
|
27
|
+
|
28
|
+
# send nice null terminated binary data
|
29
|
+
def send(data)
|
30
|
+
self.syswrite(data.to_s + "\0")
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_string
|
34
|
+
str = self.gets("\0").chop
|
35
|
+
#if str.nil?
|
36
|
+
# p 'NIL! FReaking NILLLLLLLLLLLLLLLLLLLLLLLL!'
|
37
|
+
# ''
|
38
|
+
#else
|
39
|
+
# str.chop
|
40
|
+
#end
|
41
|
+
end
|
42
|
+
|
43
|
+
def read_int
|
44
|
+
self.read_string.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_int_max
|
48
|
+
str = self.read_string
|
49
|
+
str.nil? || str.empty? ? nil : str.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_boolean
|
53
|
+
self.read_string.to_i != 0
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_decimal
|
57
|
+
# Floating-point numbers shouldn't be used to store money...
|
58
|
+
self.read_string.to_d
|
59
|
+
end
|
60
|
+
|
61
|
+
def read_decimal_max
|
62
|
+
str = self.read_string
|
63
|
+
# Floating-point numbers shouldn't be used to store money...
|
64
|
+
str.nil? || str.empty? ? nil : str.to_d
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# class IBSocket
|
69
|
+
|
70
|
+
class IB
|
71
|
+
|
72
|
+
# Please note, we are realizing only the most current TWS protocol versions,
|
73
|
+
# thus improving performance at the expense of backwards compatibility.
|
74
|
+
# Older protocol versions can be found in older gem versions.
|
75
|
+
|
76
|
+
CLIENT_VERSION = 27 # 48 drops dead # Was 27 in original Ruby code
|
77
|
+
SERVER_VERSION = 53 # Minimal server version. Latest, was 38 in current Java code.
|
78
|
+
TWS_IP_ADDRESS = "127.0.0.1"
|
79
|
+
TWS_PORT = "7496"
|
80
|
+
|
81
|
+
attr_reader :next_order_id
|
82
|
+
|
83
|
+
def initialize(options_in = {})
|
84
|
+
@options = {:ip => TWS_IP_ADDRESS, :port => TWS_PORT, }.merge(options_in)
|
85
|
+
|
86
|
+
@connected = false
|
87
|
+
@next_order_id = nil
|
88
|
+
@server = Hash.new # information about server and server connection state
|
89
|
+
|
90
|
+
# Message listeners. Key is the message class to listen for.
|
91
|
+
# Value is an Array of Procs. The proc will be called with the populated message
|
92
|
+
# instance as its argument when a message of that type is received.
|
93
|
+
@listeners = Hash.new { |hash, key| hash[key] = Array.new }
|
94
|
+
|
95
|
+
#logger.debug("IB#init: Initializing...")
|
96
|
+
|
97
|
+
self.open(@options)
|
98
|
+
end
|
99
|
+
|
100
|
+
def server_version
|
101
|
+
@server[:version]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def open(options_in = {})
|
106
|
+
raise Exception.new("Already connected!") if @connected
|
107
|
+
|
108
|
+
opts = @options.merge(options_in)
|
109
|
+
|
110
|
+
# Subscribe to the NextValidID message from TWS that is always
|
111
|
+
# sent at connect, and save the id.
|
112
|
+
self.subscribe(IncomingMessages::NextValidID) do |msg|
|
113
|
+
@next_order_id = msg.data[:id]
|
114
|
+
p "Got next valid order id #{@next_order_id}."
|
115
|
+
end
|
116
|
+
|
117
|
+
@server[:socket] = IBSocket.open(opts[:ip], opts[:port])
|
118
|
+
#logger.info("* TWS socket connected to #{@options[:ip]}:#{@options[:port]}.")
|
119
|
+
|
120
|
+
# Secret handshake.
|
121
|
+
@server[:socket].send(CLIENT_VERSION)
|
122
|
+
@server[:version] = @server[:socket].read_int
|
123
|
+
@server[:local_connect_time] = Time.now()
|
124
|
+
@@server_version = @server[:version]
|
125
|
+
raise(Exception.new("TWS version >= #{SERVER_VERSION} required.")) if @@server_version < SERVER_VERSION
|
126
|
+
|
127
|
+
puts "\tGot server version: #{@server[:version]}."
|
128
|
+
#logger.debug("\tGot server version: #{@server[:version]}.")
|
129
|
+
|
130
|
+
# Server version >= 20 sends the server time back. Our min server version is 38
|
131
|
+
@server[:remote_connect_time] = @server[:socket].read_string
|
132
|
+
#logger.debug("\tServer connect time: #{@server[:remote_connect_time]}.")
|
133
|
+
|
134
|
+
# Server wants an arbitrary client ID at this point. This can be used
|
135
|
+
# to identify subsequent communications.
|
136
|
+
@server[:client_id] = SHA1.digest(Time.now.to_s + $$.to_s).unpack("C*").join.to_i % 999999999
|
137
|
+
@server[:socket].send(@server[:client_id])
|
138
|
+
#logger.debug("\tSent client id # #{@server[:client_id]}.")
|
139
|
+
|
140
|
+
#logger.debug("Starting reader thread..")
|
141
|
+
Thread.abort_on_exception = true
|
142
|
+
@server[:reader_thread] = Thread.new { self.reader }
|
143
|
+
|
144
|
+
@connected = true
|
145
|
+
end
|
146
|
+
|
147
|
+
def close
|
148
|
+
@server[:reader_thread].kill # Thread uses blocking I/O, so join is useless.
|
149
|
+
@server[:socket].close()
|
150
|
+
@server = Hash.new
|
151
|
+
@@server_version = nil
|
152
|
+
@connected = false
|
153
|
+
#logger.debug("Disconnected.")
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_s
|
157
|
+
"IB Connector: #{ @connected ? "connected." : "disconnected."}"
|
158
|
+
end
|
159
|
+
|
160
|
+
# Subscribe to incoming message events of type message_class.
|
161
|
+
# code is a Proc that will be called with the message instance as its argument.
|
162
|
+
def subscribe(message_class, code = nil, &block)
|
163
|
+
code ||= block
|
164
|
+
|
165
|
+
raise ArgumentError.new "Need listener proc or block" unless code.is_a? Proc
|
166
|
+
unless message_class < IncomingMessages::AbstractMessage
|
167
|
+
raise ArgumentError.new "#{message_class} must be an IB message class"
|
168
|
+
end
|
169
|
+
|
170
|
+
@listeners[message_class].push(code)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Send an outgoing message.
|
174
|
+
def dispatch(message)
|
175
|
+
raise Exception.new("dispatch() must be given an OutgoingMessages::AbstractMessage subclass") unless message.is_a?(OutgoingMessages::AbstractMessage)
|
176
|
+
|
177
|
+
#logger.info("Sending message " + message.inspect)
|
178
|
+
message.send(@server)
|
179
|
+
end
|
180
|
+
|
181
|
+
protected
|
182
|
+
|
183
|
+
def reader
|
184
|
+
#logger.debug("Reader started.")
|
185
|
+
|
186
|
+
while true
|
187
|
+
# this blocks, so Thread#join is useless.
|
188
|
+
msg_id = @server[:socket].read_int
|
189
|
+
|
190
|
+
p "Reader: got message id #{msg_id}." unless [4, 6, 7, 8, 53].include? msg_id
|
191
|
+
|
192
|
+
if msg_id == 0
|
193
|
+
p "Zero msg id! Must be a nil passed in... Ignoring..."
|
194
|
+
else
|
195
|
+
# Create a new instance of the appropriate message type, and have it read the message.
|
196
|
+
# NB: Failure here usually means unsupported message type received
|
197
|
+
msg = IncomingMessages::Table[msg_id].new(@server[:socket], @server[:version])
|
198
|
+
|
199
|
+
@listeners[msg.class].each { |listener|
|
200
|
+
listener.call(msg)
|
201
|
+
}
|
202
|
+
|
203
|
+
#logger.debug { " Listeners: #{@listeners.inspect} inclusion: #{ @listeners.include?(msg.class)}" }
|
204
|
+
|
205
|
+
# Log the error messages. Make an exception for the "successfully connected"
|
206
|
+
# messages, which, for some reason, come back from IB as errors.
|
207
|
+
if msg.is_a?(IncomingMessages::Error)
|
208
|
+
# connect strings
|
209
|
+
if msg.code == 2104 || msg.code == 2106
|
210
|
+
#logger.info(msg.to_human)
|
211
|
+
else
|
212
|
+
#logger.error(msg.to_human)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
# Warn if nobody listened to a non-error incoming message.
|
216
|
+
unless @listeners[msg.class].size > 0
|
217
|
+
#logger.warn { " WARNING: Nobody listened to incoming message #{msg.class}" }
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
# #logger.debug("Reader done with message id #{msg_id}.")
|
222
|
+
end # while
|
223
|
+
end # reader
|
224
|
+
|
225
|
+
end # class IB
|
226
|
+
end # module IB
|