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/bin/time_and_sales
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This script connects to IB API and subscribes to market data for specific symbols.
|
4
|
+
# It then prints out all trades that exceed certain size.
|
5
|
+
|
6
|
+
require 'pathname'
|
7
|
+
LIB_DIR = (Pathname.new(__FILE__).dirname + '../lib/').realpath.to_s
|
8
|
+
$LOAD_PATH.unshift LIB_DIR unless $LOAD_PATH.include?(LIB_DIR)
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
require 'bundler/setup'
|
12
|
+
require 'ib-ruby'
|
13
|
+
|
14
|
+
# Define the symbols we're interested in.
|
15
|
+
@market = {
|
16
|
+
123 => IB::Symbols::Futures[:gbp],
|
17
|
+
234 => IB::Symbols::Futures[:jpy]
|
18
|
+
}
|
19
|
+
|
20
|
+
# To determine when the timeout has passed.
|
21
|
+
@last_msg_time = Time.now.to_i + 2
|
22
|
+
|
23
|
+
# Connect to IB TWS.
|
24
|
+
ib = IB::Connection.new
|
25
|
+
|
26
|
+
# Subscribe to TWS alerts/errors
|
27
|
+
ib.subscribe(:Alert) { |msg| puts msg.to_human }
|
28
|
+
|
29
|
+
MIN_SIZE = 0
|
30
|
+
|
31
|
+
# This method filters out non-:last type events, and filters out any sale < MIN_SIZE.
|
32
|
+
# Note that we have to look the ticker id of each incoming message
|
33
|
+
# up in local memory to figure out what it's for.
|
34
|
+
# (N.B. The description field is not from IB TWS. It is defined
|
35
|
+
# locally in forex.rb, and is just arbitrary text.)
|
36
|
+
def show_sales_and_size(msg)
|
37
|
+
#return if msg.type != :last_price || msg.data[:size] < MIN_SIZE
|
38
|
+
puts @market[msg.data[:id]].description + ": " +
|
39
|
+
(msg.is_a?(IB::Messages::Incoming::TickPrice) ?
|
40
|
+
"#{msg.data[:size]} at #{msg.data[:price]}" : msg.to_human)
|
41
|
+
end
|
42
|
+
|
43
|
+
def show_size(msg)
|
44
|
+
return if msg.type != :last_size || msg.data[:size] < MIN_SIZE
|
45
|
+
end
|
46
|
+
|
47
|
+
# Now, subscribe to TickerPrice and TickerSize events. The code passed in the block
|
48
|
+
# will be executed when a message of that type is received, with the received message
|
49
|
+
# as its argument. In this case, we just print out the tick.
|
50
|
+
ib.subscribe(:TickPrice, :TickSize, :TickString) { |msg| show_sales_and_size(msg) }
|
51
|
+
|
52
|
+
# Now we actually request market data for the symbols we're interested in.
|
53
|
+
|
54
|
+
@market.each_pair do |id, contract|
|
55
|
+
ib.send :RequestMarketData, :id => id, :contract => contract
|
56
|
+
end
|
57
|
+
|
58
|
+
puts "\nSubscribed to TWS market data"
|
59
|
+
puts "\n******** Press <Enter> to cancel... *********\n\n"
|
60
|
+
gets
|
61
|
+
puts "Unsubscribing from TWS market data.."
|
62
|
+
|
63
|
+
@market.each_pair { |id, contract| ib.send :CancelMarketData, :id => id }
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'ib-ruby/socket'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
if RUBY_VERSION < "1.9"
|
5
|
+
require 'sha1'
|
6
|
+
else
|
7
|
+
require 'digest/sha1'
|
8
|
+
include Digest
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add method to_ib to render datetime in IB format (zero padded "yyyymmdd HH:mm:ss")
|
12
|
+
class Time
|
13
|
+
def to_ib
|
14
|
+
"#{self.year}#{sprintf("%02d", self.month)}#{sprintf("%02d", self.day)} " +
|
15
|
+
"#{sprintf("%02d", self.hour)}:#{sprintf("%02d", self.min)}:#{sprintf("%02d", self.sec)}"
|
16
|
+
end
|
17
|
+
end # Time
|
18
|
+
|
19
|
+
module IB
|
20
|
+
# Encapsulates API connection to TWS or Gateway
|
21
|
+
class Connection
|
22
|
+
|
23
|
+
# Please note, we are realizing only the most current TWS protocol versions,
|
24
|
+
# thus improving performance at the expense of backwards compatibility.
|
25
|
+
# Older protocol versions support can be found in older gem versions.
|
26
|
+
|
27
|
+
CLIENT_VERSION = 48 # Was 27 in original Ruby code
|
28
|
+
SERVER_VERSION = 53 # Minimal server version. Latest, was 38 in current Java code.
|
29
|
+
DEFAULT_OPTIONS = {:host =>"127.0.0.1",
|
30
|
+
:port => '4001', # Gateway, TWS: '7496'
|
31
|
+
:open => true
|
32
|
+
}
|
33
|
+
|
34
|
+
attr_reader :next_order_id
|
35
|
+
|
36
|
+
def initialize(opts = {})
|
37
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
38
|
+
|
39
|
+
@connected = false
|
40
|
+
@next_order_id = nil
|
41
|
+
@server = Hash.new # information about server and server connection state
|
42
|
+
|
43
|
+
# Message listeners. Key is the message class to listen for.
|
44
|
+
# Value is an Array of Procs. The proc will be called with the populated message
|
45
|
+
# instance as its argument when a message of that type is received.
|
46
|
+
# TODO: change Array of Procs into a Hash to allow unsubscribing
|
47
|
+
@listeners = Hash.new { |hash, key| hash[key] = Array.new }
|
48
|
+
|
49
|
+
self.open(@options) if @options[:open]
|
50
|
+
end
|
51
|
+
|
52
|
+
def server_version
|
53
|
+
@server[:version]
|
54
|
+
end
|
55
|
+
|
56
|
+
def open(opts = {})
|
57
|
+
raise Exception.new("Already connected!") if @connected
|
58
|
+
|
59
|
+
opts = @options.merge(opts)
|
60
|
+
|
61
|
+
# Subscribe to the NextValidID message from TWS that is always
|
62
|
+
# sent at connect, and save the id.
|
63
|
+
self.subscribe(Messages::Incoming::NextValidID) do |msg|
|
64
|
+
@next_order_id = msg.data[:id]
|
65
|
+
puts "Got next valid order id #{@next_order_id}."
|
66
|
+
end
|
67
|
+
|
68
|
+
@server[:socket] = IBSocket.open(opts[:host], opts[:port])
|
69
|
+
|
70
|
+
# Secret handshake.
|
71
|
+
@server[:socket].send(CLIENT_VERSION)
|
72
|
+
@server[:version] = @server[:socket].read_int
|
73
|
+
@server[:local_connect_time] = Time.now()
|
74
|
+
raise(Exception.new("TWS version >= #{SERVER_VERSION} required.")) if @server[:version] < SERVER_VERSION
|
75
|
+
|
76
|
+
puts "\tGot server version: #{@server[:version]}."
|
77
|
+
#logger.debug("\tGot server version: #{@server[:version]}.")
|
78
|
+
|
79
|
+
# Server version >= 20 sends the server time back. Our min server version is 38
|
80
|
+
@server[:remote_connect_time] = @server[:socket].read_string
|
81
|
+
#logger.debug("\tServer connect time: #{@server[:remote_connect_time]}.")
|
82
|
+
|
83
|
+
# Server wants an arbitrary client ID at this point. This can be used
|
84
|
+
# to identify subsequent communications.
|
85
|
+
@server[:client_id] = SHA1.digest(Time.now.to_s + $$.to_s).unpack("C*").join.to_i % 999999999
|
86
|
+
@server[:socket].send(@server[:client_id])
|
87
|
+
#logger.debug("\tSent client id # #{@server[:client_id]}.")
|
88
|
+
|
89
|
+
#logger.debug("Starting reader thread..")
|
90
|
+
Thread.abort_on_exception = true
|
91
|
+
@server[:reader_thread] = Thread.new { self.reader }
|
92
|
+
|
93
|
+
@connected = true
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
@server[:reader_thread].kill # Thread uses blocking I/O, so join is useless.
|
98
|
+
@server[:socket].close()
|
99
|
+
@server = Hash.new
|
100
|
+
@@server_version = nil
|
101
|
+
@connected = false
|
102
|
+
#logger.debug("Disconnected.")
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_s
|
106
|
+
"IB Connector: #{ @connected ? "connected." : "disconnected."}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Subscribe to incoming message events of type message_class.
|
110
|
+
# code is a Proc that will be called with the message instance as its argument.
|
111
|
+
def subscribe(*args, &block)
|
112
|
+
code = args.last.respond_to?(:call) ? args.pop : block
|
113
|
+
|
114
|
+
raise ArgumentError.new "Need listener proc or block" unless code.is_a? Proc
|
115
|
+
|
116
|
+
args.each do |message_class|
|
117
|
+
if message_class.is_a? Symbol
|
118
|
+
message_class = Messages::Incoming.const_get(message_class)
|
119
|
+
end
|
120
|
+
|
121
|
+
unless message_class < Messages::Incoming::AbstractMessage
|
122
|
+
raise ArgumentError.new "#{message_class} must be an IB message class"
|
123
|
+
end
|
124
|
+
|
125
|
+
@listeners[message_class].push(code)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Send an outgoing message.
|
130
|
+
def send(message, *args)
|
131
|
+
if message.is_a? Symbol
|
132
|
+
message = Messages::Outgoing.const_get(message).new *args
|
133
|
+
end
|
134
|
+
|
135
|
+
raise Exception.new("only sending Messages::Outgoing") unless message.is_a? Messages::Outgoing::AbstractMessage
|
136
|
+
|
137
|
+
message.send(@server)
|
138
|
+
end
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
def reader
|
143
|
+
loop do
|
144
|
+
# this blocks, so Thread#join is useless.
|
145
|
+
msg_id = @server[:socket].read_int
|
146
|
+
|
147
|
+
# Debug:
|
148
|
+
unless [1, 2, 4, 6, 7, 8, 9, 21, 53].include? msg_id
|
149
|
+
puts "Got message #{msg_id} (#{Messages::Incoming::Table[msg_id]})"
|
150
|
+
end
|
151
|
+
|
152
|
+
# Create a new instance of the appropriate message type, and have it read the message.
|
153
|
+
# NB: Failure here usually means unsupported message type received
|
154
|
+
msg = Messages::Incoming::Table[msg_id].new(@server[:socket], @server[:version])
|
155
|
+
|
156
|
+
if @listeners[msg.class].size > 0
|
157
|
+
@listeners[msg.class].each { |listener| listener.call(msg) }
|
158
|
+
else
|
159
|
+
# Warn if nobody listened to an incoming message.
|
160
|
+
puts " WARNING: Nobody listened to incoming message #{msg.class}"
|
161
|
+
end
|
162
|
+
end # loop
|
163
|
+
end # reader
|
164
|
+
end # class Connection
|
165
|
+
IB = Connection # Legacy alias
|
166
|
+
end # module IB
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module IB
|
2
|
+
### Widely used TWS constants:
|
3
|
+
|
4
|
+
EOL = "\0"
|
5
|
+
|
6
|
+
#FaMsgTypeName = {1 => "GROUPS",
|
7
|
+
# 2 => "PROFILES",
|
8
|
+
# 3 =>"ALIASES"}
|
9
|
+
|
10
|
+
# Enumeration of data types
|
11
|
+
DATA_TYPES = [:trades, :midpoint, :bid, :ask]
|
12
|
+
|
13
|
+
# Enumeration of bar size types for convenience.
|
14
|
+
# Bar sizes less than 30 seconds do not work for some securities.
|
15
|
+
BAR_SIZES = ['1 sec', '5 secs', '15 secs', '30 secs',
|
16
|
+
'1 min', '2 mins', '3 mins', '5 mins',
|
17
|
+
'15 mins', '30 mins', '1 hour', '1 day']
|
18
|
+
|
19
|
+
# Tick types as received in TickPrice and TickSize messages (enumeration)
|
20
|
+
TICK_TYPES = {
|
21
|
+
# int id => :Description # Corresponding API Event/Function/Method
|
22
|
+
0 => :bid_size, # tickSize()
|
23
|
+
1 => :bid_price, # tickPrice()
|
24
|
+
2 => :ask_price, # tickPrice()
|
25
|
+
3 => :ask_size, # tickSize()
|
26
|
+
4 => :last_price, # tickPrice()
|
27
|
+
5 => :last_size, # tickSize()
|
28
|
+
6 => :high, # tickPrice()
|
29
|
+
7 => :low, # tickPrice()
|
30
|
+
8 => :volume, # tickSize()
|
31
|
+
9 => :close_price, # tickPrice()
|
32
|
+
10 => :bid_option, # tickOptionComputation() See Note 1 below
|
33
|
+
11 => :ask_option, # tickOptionComputation() See => :Note 1 below
|
34
|
+
12 => :last_option, # tickOptionComputation() See Note 1 below
|
35
|
+
13 => :model_option, # tickOptionComputation() See Note 1 below
|
36
|
+
14 => :open_tick, # tickPrice()
|
37
|
+
15 => :low_13_week, # tickPrice()
|
38
|
+
16 => :high_13_week, # tickPrice()
|
39
|
+
17 => :low_26_week, # tickPrice()
|
40
|
+
18 => :high_26_week, # tickPrice()
|
41
|
+
19 => :low_52_week, # tickPrice()
|
42
|
+
20 => :high_52_week, # tickPrice()
|
43
|
+
21 => :avg_volume, # tickSize()
|
44
|
+
22 => :open_interest, # tickSize()
|
45
|
+
23 => :option_historical_vol, # tickGeneric()
|
46
|
+
24 => :option_implied_vol, # tickGeneric()
|
47
|
+
25 => :option_bid_exch, # not USED
|
48
|
+
26 => :option_ask_exch, # not USED
|
49
|
+
27 => :option_call_open_interest, # tickSize()
|
50
|
+
28 => :option_put_open_interest, # tickSize()
|
51
|
+
29 => :option_call_volume, # tickSize()
|
52
|
+
30 => :option_put_volume, # tickSize()
|
53
|
+
31 => :index_future_premium, # tickGeneric()
|
54
|
+
32 => :bid_exch, # tickString()
|
55
|
+
33 => :ask_exch, # tickString()
|
56
|
+
34 => :auction_volume, # not USED
|
57
|
+
35 => :auction_price, # not USED
|
58
|
+
36 => :auction_imbalance, # not USED
|
59
|
+
37 => :mark_price, # tickPrice()
|
60
|
+
38 => :bid_efp_computation, # tickEFP()
|
61
|
+
39 => :ask_efp_computation, # tickEFP()
|
62
|
+
40 => :last_efp_computation, # tickEFP()
|
63
|
+
41 => :open_efp_computation, # tickEFP()
|
64
|
+
42 => :high_efp_computation, # tickEFP()
|
65
|
+
43 => :low_efp_computation, # tickEFP()
|
66
|
+
44 => :close_efp_computation, # tickEFP()
|
67
|
+
45 => :last_timestamp, # tickString()
|
68
|
+
46 => :shortable, # tickGeneric()
|
69
|
+
47 => :fundamental_ratios, # tickString()
|
70
|
+
48 => :rt_volume, # tickGeneric()
|
71
|
+
49 => :halted, # see Note 2 below.
|
72
|
+
50 => :bid_yield, # tickPrice() See Note 3 below
|
73
|
+
51 => :asky_ield, # tickPrice() See Note 3 below
|
74
|
+
52 => :last_yield, # tickPrice() See Note 3 below
|
75
|
+
53 => :cust_option_computation, # tickOptionComputation()
|
76
|
+
54 => :trade_count, # tickGeneric()
|
77
|
+
55 => :trade_rate, # tickGeneric()
|
78
|
+
56 => :volume_rate, # tickGeneric()
|
79
|
+
57 => :last_rth_trade, # ?
|
80
|
+
# Note 1: Tick types BID_OPTION, ASK_OPTION, LAST_OPTION, and MODEL_OPTION return
|
81
|
+
# all Greeks (delta, gamma, vega, theta), the underlying price and the
|
82
|
+
# stock and option reference price when requested.
|
83
|
+
# MODEL_OPTION also returns model implied volatility.
|
84
|
+
# Note 2: When trading is halted for a contract, TWS receives a special tick:
|
85
|
+
# haltedLast=1. When trading is resumed, TWS receives haltedLast=0.
|
86
|
+
# A tick type, HALTED, tick ID= 49, is now available in regular market
|
87
|
+
# data via the API to indicate this halted state. Possible values for
|
88
|
+
# this new tick type are: 0 = Not halted, 1 = Halted.
|
89
|
+
# Note 3: Applies to bond contracts only.
|
90
|
+
}
|
91
|
+
end # module IB
|