my-ib-api 0.0.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/lib/ib-api.rb +10 -0
- data/lib/ib/base.rb +99 -0
- data/lib/ib/base_properties.rb +154 -0
- data/lib/ib/connection.rb +327 -0
- data/lib/ib/constants.rb +334 -0
- data/lib/ib/db.rb +29 -0
- data/lib/ib/engine.rb +35 -0
- data/lib/ib/errors.rb +40 -0
- data/lib/ib/extensions.rb +72 -0
- data/lib/ib/flex.rb +106 -0
- data/lib/ib/logger.rb +25 -0
- data/lib/ib/messages.rb +88 -0
- data/lib/ib/messages/abstract_message.rb +89 -0
- data/lib/ib/messages/incoming.rb +134 -0
- data/lib/ib/messages/incoming/abstract_message.rb +99 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +102 -0
- data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
- data/lib/ib/messages/incoming/execution_data.rb +54 -0
- data/lib/ib/messages/incoming/historical_data.rb +55 -0
- data/lib/ib/messages/incoming/market_depths.rb +44 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +18 -0
- data/lib/ib/messages/incoming/open_order.rb +232 -0
- data/lib/ib/messages/incoming/order_status.rb +81 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +39 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/scanner_data.rb +53 -0
- data/lib/ib/messages/incoming/ticks.rb +131 -0
- data/lib/ib/messages/outgoing.rb +331 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +73 -0
- data/lib/ib/messages/outgoing/bar_requests.rb +189 -0
- data/lib/ib/messages/outgoing/place_order.rb +141 -0
- data/lib/ib/model.rb +6 -0
- data/lib/ib/models.rb +10 -0
- data/lib/ib/requires.rb +9 -0
- data/lib/ib/socket.rb +81 -0
- data/lib/ib/symbols.rb +35 -0
- data/lib/ib/symbols/bonds.rb +28 -0
- data/lib/ib/symbols/forex.rb +41 -0
- data/lib/ib/symbols/futures.rb +117 -0
- data/lib/ib/symbols/options.rb +39 -0
- data/lib/ib/symbols/stocks.rb +37 -0
- data/lib/ib/version.rb +6 -0
- data/lib/models/ib/bag.rb +51 -0
- data/lib/models/ib/bar.rb +45 -0
- data/lib/models/ib/combo_leg.rb +103 -0
- data/lib/models/ib/contract.rb +292 -0
- data/lib/models/ib/contract_detail.rb +89 -0
- data/lib/models/ib/execution.rb +65 -0
- data/lib/models/ib/option.rb +60 -0
- data/lib/models/ib/order.rb +391 -0
- data/lib/models/ib/order_state.rb +128 -0
- data/lib/models/ib/underlying.rb +34 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 792fc49c490a522dbe2f5bb42ed76c25f72257ad309fad068e2422d7cf7cc320
|
4
|
+
data.tar.gz: c6182a9d72d900a820d0493db2c7f8b8eac9d07c42b826f658d64e47ffa2d5d4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f96ed40918e1a8ddf63c57618f2b18c84ecfa4ccef23f92aa3073d5bf30dfe4c11b51d962f2eb7e2bbd852b0dbc16f1e4e3441ede1bf4dce5c8cc0ef0da4f966
|
7
|
+
data.tar.gz: 366d962ba84ae4c899d9c0abf897830a2f62d48c9a5a97b14572aba10f9f28d664084db39b95e311c90441a328e8e1c3b41c6e563c4fa7cb0a3eaddfd675c5c8
|
data/lib/ib-api.rb
ADDED
data/lib/ib/base.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module IB
|
2
|
+
|
3
|
+
# Base class for tableless IB data Models, extends ActiveModel API
|
4
|
+
class Base
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
extend ActiveModel::Callbacks
|
7
|
+
include ActiveModel::Validations
|
8
|
+
include ActiveModel::Serialization
|
9
|
+
### include ActiveModel::Serializers::Xml
|
10
|
+
### include ActiveModel::Serializers::JSON
|
11
|
+
|
12
|
+
define_model_callbacks :initialize
|
13
|
+
|
14
|
+
# If a opts hash is given, keys are taken as attribute names, values as data.
|
15
|
+
# The model instance fields are then set automatically from the opts Hash.
|
16
|
+
def initialize attributes={}, opts={}
|
17
|
+
run_callbacks :initialize do
|
18
|
+
error "Argument must be a Hash", :args unless attributes.is_a?(Hash)
|
19
|
+
|
20
|
+
self.attributes = attributes # set_attribute_defaults is now after_init callback
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# ActiveModel API (for serialization)
|
25
|
+
|
26
|
+
def attributes
|
27
|
+
@attributes ||= HashWithIndifferentAccess.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes= attrs
|
31
|
+
attrs.keys.each { |key| self.send("#{key}=", attrs[key]) }
|
32
|
+
end
|
33
|
+
|
34
|
+
# ActiveModel-style read/write_attribute accessors
|
35
|
+
def [] key
|
36
|
+
attributes[key.to_sym]
|
37
|
+
end
|
38
|
+
|
39
|
+
def []= key, val
|
40
|
+
# p key, val
|
41
|
+
attributes[key.to_sym] = val
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_model
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def new_record?
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def save
|
53
|
+
valid?
|
54
|
+
end
|
55
|
+
|
56
|
+
alias save! save
|
57
|
+
|
58
|
+
### Noop methods mocking ActiveRecord::Base macros
|
59
|
+
|
60
|
+
def self.attr_protected *args
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.attr_accessible *args
|
64
|
+
end
|
65
|
+
|
66
|
+
### ActiveRecord::Base association API mocks
|
67
|
+
|
68
|
+
def self.belongs_to model, *args
|
69
|
+
attr_accessor model
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.has_one model, *args
|
73
|
+
attr_accessor model
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.has_many models, *args
|
77
|
+
attr_accessor models
|
78
|
+
|
79
|
+
define_method(models) do
|
80
|
+
self.instance_variable_get("@#{models}") ||
|
81
|
+
self.instance_variable_set("@#{models}", [])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.find *args
|
86
|
+
[]
|
87
|
+
end
|
88
|
+
|
89
|
+
### ActiveRecord::Base callback API mocks
|
90
|
+
|
91
|
+
define_model_callbacks :initialize, :only => :after
|
92
|
+
|
93
|
+
### ActiveRecord::Base misc
|
94
|
+
|
95
|
+
def self.serialize *properties
|
96
|
+
end
|
97
|
+
|
98
|
+
end # Model
|
99
|
+
end # module IB
|
@@ -0,0 +1,154 @@
|
|
1
|
+
|
2
|
+
require 'active_model'
|
3
|
+
#require "active_model_serializers"
|
4
|
+
|
5
|
+
require 'active_support/concern'
|
6
|
+
require 'active_support/hash_with_indifferent_access'
|
7
|
+
|
8
|
+
module IB
|
9
|
+
|
10
|
+
# Module adds prop Macro and
|
11
|
+
module BaseProperties
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
### Instance methods
|
15
|
+
|
16
|
+
# Default presentation
|
17
|
+
def to_human
|
18
|
+
"<#{self.class.to_s.demodulize}: " + attributes.map do |attr, value|
|
19
|
+
"#{attr}: #{value}" unless value.nil?
|
20
|
+
end.compact.sort.join(' ') + ">"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Comparison support
|
24
|
+
def content_attributes
|
25
|
+
HashWithIndifferentAccess[attributes.reject do |(attr, _)|
|
26
|
+
attr.to_s =~ /(_count)\z/ ||
|
27
|
+
[:created_at, :updated_at, :type,
|
28
|
+
:id, :order_id, :contract_id].include?(attr.to_sym)
|
29
|
+
end]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Update nil attributes from given Hash or model
|
33
|
+
def update_missing attrs
|
34
|
+
attrs = attrs.content_attributes unless attrs.kind_of?(Hash)
|
35
|
+
|
36
|
+
attrs.each { |attr, val| send "#{attr}=", val if send(attr).blank? }
|
37
|
+
self # for chaining
|
38
|
+
end
|
39
|
+
|
40
|
+
# Default Model comparison
|
41
|
+
def == other
|
42
|
+
case other
|
43
|
+
when String # Probably a Rails URI, delegate to AR::Base
|
44
|
+
super(other)
|
45
|
+
else
|
46
|
+
content_attributes.keys.inject(true) { |res, key|
|
47
|
+
res && other.respond_to?(key) && (send(key) == other.send(key)) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
### Default attributes support
|
52
|
+
|
53
|
+
def default_attributes
|
54
|
+
{:created_at => Time.now,
|
55
|
+
:updated_at => Time.now,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_attribute_defaults
|
60
|
+
default_attributes.each do |key, val|
|
61
|
+
self.send("#{key}=", val) if self.send(key).nil?
|
62
|
+
# self.send("#{key}=", val) if self[key].nil? # Problems with association defaults
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
included do
|
67
|
+
|
68
|
+
after_initialize :set_attribute_defaults
|
69
|
+
|
70
|
+
### Class macros
|
71
|
+
|
72
|
+
def self.prop *properties
|
73
|
+
prop_hash = properties.last.is_a?(Hash) ? properties.pop : {}
|
74
|
+
|
75
|
+
properties.each { |names| define_property names, nil }
|
76
|
+
prop_hash.each { |names, type| define_property names, type }
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.define_property names, body
|
80
|
+
aliases = [names].flatten
|
81
|
+
name = aliases.shift
|
82
|
+
instance_eval do
|
83
|
+
|
84
|
+
define_property_methods name, body
|
85
|
+
|
86
|
+
aliases.each do |ali|
|
87
|
+
alias_method "#{ali}", name
|
88
|
+
alias_method "#{ali}=", "#{name}="
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.define_property_methods name, body={}
|
94
|
+
#p name, body
|
95
|
+
case body
|
96
|
+
when '' # default getter and setter
|
97
|
+
define_property_methods name
|
98
|
+
|
99
|
+
when Array # [setter, getter, validators]
|
100
|
+
define_property_methods name,
|
101
|
+
:get => body[0],
|
102
|
+
:set => body[1],
|
103
|
+
:validate => body[2]
|
104
|
+
|
105
|
+
when Hash # recursion base case
|
106
|
+
getter = case # Define getter
|
107
|
+
when body[:get].respond_to?(:call)
|
108
|
+
body[:get]
|
109
|
+
when body[:get]
|
110
|
+
proc { self[name].send "to_#{body[:get]}" }
|
111
|
+
when VALUES[name] # property is encoded
|
112
|
+
proc { VALUES[name][self[name]] }
|
113
|
+
else
|
114
|
+
proc { self[name] }
|
115
|
+
end
|
116
|
+
define_method name, &getter if getter
|
117
|
+
|
118
|
+
setter = case # Define setter
|
119
|
+
when body[:set].respond_to?(:call)
|
120
|
+
body[:set]
|
121
|
+
when body[:set]
|
122
|
+
proc { |value| self[name] = value.send "to_#{body[:set]}" }
|
123
|
+
when CODES[name] # property is encoded
|
124
|
+
proc { |value| self[name] = CODES[name][value] || value }
|
125
|
+
else
|
126
|
+
proc { |value| self[name] = value } # p name, value;
|
127
|
+
end
|
128
|
+
define_method "#{name}=", &setter if setter
|
129
|
+
|
130
|
+
# Define validator(s)
|
131
|
+
[body[:validate]].flatten.compact.each do |validator|
|
132
|
+
case validator
|
133
|
+
when Proc
|
134
|
+
validates_each name, &validator
|
135
|
+
when Hash
|
136
|
+
validates name, validator.dup
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# TODO define self[:name] accessors for :virtual and :flag properties
|
141
|
+
|
142
|
+
else # setter given
|
143
|
+
define_property_methods name, :set => body, :get => body
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Timestamps in lightweight models
|
148
|
+
unless defined?(ActiveRecord::Base) && ancestors.include?(ActiveRecord::Base)
|
149
|
+
prop :created_at, :updated_at
|
150
|
+
end
|
151
|
+
|
152
|
+
end # included
|
153
|
+
end # module BaseProperties
|
154
|
+
end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'ib/socket'
|
3
|
+
require 'ib/logger'
|
4
|
+
require 'ib/messages'
|
5
|
+
|
6
|
+
module IB
|
7
|
+
# Encapsulates API connection to TWS or Gateway
|
8
|
+
class Connection
|
9
|
+
|
10
|
+
# Please note, we are realizing only the most current TWS protocol versions,
|
11
|
+
# thus improving performance at the expense of backwards compatibility.
|
12
|
+
# Older protocol versions support can be found in older gem versions.
|
13
|
+
|
14
|
+
DEFAULT_OPTIONS = {:host =>'127.0.0.1',
|
15
|
+
:port => '4001', # IB Gateway connection (default)
|
16
|
+
#:port => '7496', # TWS connection
|
17
|
+
:connect => true, # Connect at initialization
|
18
|
+
:reader => true, # Start a separate reader Thread
|
19
|
+
:received => true, # Keep all received messages in a @received Hash
|
20
|
+
:logger => nil,
|
21
|
+
:client_id => nil, # Will be randomly assigned
|
22
|
+
:client_version => IB::Messages::CLIENT_VERSION,
|
23
|
+
:server_version => IB::Messages::SERVER_VERSION
|
24
|
+
}
|
25
|
+
|
26
|
+
# Singleton to make active Connection universally accessible as IB::Connection.current
|
27
|
+
class << self
|
28
|
+
attr_accessor :current
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_accessor :options, # Connection options
|
32
|
+
:socket, # Socket to IB server (TWS or Gateway)
|
33
|
+
:reader, # Reader thread
|
34
|
+
:client_version,
|
35
|
+
:server_version,
|
36
|
+
:remote_connect_time,
|
37
|
+
:local_connect_time,
|
38
|
+
:client_id, # Client id of this Connection (as seen bu IB server)
|
39
|
+
:next_local_id # Next valid order id
|
40
|
+
|
41
|
+
alias next_order_id next_local_id
|
42
|
+
alias next_order_id= next_local_id=
|
43
|
+
|
44
|
+
def initialize opts = {}
|
45
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
46
|
+
|
47
|
+
# A couple of locks to avoid race conditions in JRuby
|
48
|
+
@subscribe_lock = Mutex.new
|
49
|
+
@receive_lock = Mutex.new
|
50
|
+
|
51
|
+
self.default_logger = options[:logger] if options[:logger]
|
52
|
+
@connected = false
|
53
|
+
self.next_local_id = nil
|
54
|
+
|
55
|
+
connect if options[:connect]
|
56
|
+
Connection.current = self
|
57
|
+
end
|
58
|
+
|
59
|
+
### Working with connection
|
60
|
+
|
61
|
+
def connect
|
62
|
+
error "Already connected!" if connected?
|
63
|
+
|
64
|
+
# TWS always sends NextValidId message at connect - save this id
|
65
|
+
self.subscribe(:NextValidId) do |msg|
|
66
|
+
self.next_local_id = msg.local_id
|
67
|
+
log.info "Got next valid order id: #{next_local_id}."
|
68
|
+
end
|
69
|
+
|
70
|
+
@socket = IBSocket.open(options[:host], options[:port])
|
71
|
+
|
72
|
+
# Secret handshake
|
73
|
+
@client_version = options[:client_version]
|
74
|
+
socket.write_data @client_version
|
75
|
+
@server_version = socket.read_int
|
76
|
+
if @server_version < options[:server_version]
|
77
|
+
error "Server version #{@server_version}, #{options[:server_version]} required."
|
78
|
+
end
|
79
|
+
@remote_connect_time = socket.read_string
|
80
|
+
@local_connect_time = Time.now
|
81
|
+
|
82
|
+
# Sending (arbitrary) client ID to identify subsequent communications.
|
83
|
+
# The client with a client_id of 0 can manage the TWS-owned open orders.
|
84
|
+
# Other clients can only manage their own open orders.
|
85
|
+
@client_id = options[:client_id] || random_id
|
86
|
+
socket.write_data @client_id
|
87
|
+
|
88
|
+
@connected = true
|
89
|
+
log.info "Connected to server, ver: #{@server_version}, connection time: " +
|
90
|
+
"#{@local_connect_time} local, " +
|
91
|
+
"#{@remote_connect_time} remote."
|
92
|
+
|
93
|
+
start_reader if options[:reader] # Allows reconnect
|
94
|
+
end
|
95
|
+
|
96
|
+
alias open connect # Legacy alias
|
97
|
+
|
98
|
+
def disconnect
|
99
|
+
if reader_running?
|
100
|
+
@reader_running = false
|
101
|
+
@reader.join
|
102
|
+
end
|
103
|
+
if connected?
|
104
|
+
socket.close
|
105
|
+
@connected = false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
alias close disconnect # Legacy alias
|
110
|
+
|
111
|
+
def connected?
|
112
|
+
@connected
|
113
|
+
end
|
114
|
+
|
115
|
+
### Working with message subscribers
|
116
|
+
|
117
|
+
# Subscribe Proc or block to specific type(s) of incoming message events.
|
118
|
+
# Listener will be called later with received message instance as its argument.
|
119
|
+
# Returns subscriber id to allow unsubscribing
|
120
|
+
def subscribe *args, &block
|
121
|
+
@subscribe_lock.synchronize do
|
122
|
+
subscriber = args.last.respond_to?(:call) ? args.pop : block
|
123
|
+
id = random_id
|
124
|
+
|
125
|
+
error "Need subscriber proc or block", :args unless subscriber.is_a? Proc
|
126
|
+
|
127
|
+
args.each do |what|
|
128
|
+
message_classes =
|
129
|
+
case
|
130
|
+
when what.is_a?(Class) && what < Messages::Incoming::AbstractMessage
|
131
|
+
[what]
|
132
|
+
when what.is_a?(Symbol)
|
133
|
+
[Messages::Incoming.const_get(what)]
|
134
|
+
when what.is_a?(Regexp)
|
135
|
+
Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what }
|
136
|
+
else
|
137
|
+
error "#{what} must represent incoming IB message class", :args
|
138
|
+
end
|
139
|
+
message_classes.flatten.each do |message_class|
|
140
|
+
# TODO: Fix: RuntimeError: can't add a new key into hash during iteration
|
141
|
+
subscribers[message_class][id] = subscriber
|
142
|
+
end
|
143
|
+
end
|
144
|
+
id
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Remove all subscribers with specific subscriber id (TODO: multiple ids)
|
149
|
+
def unsubscribe *ids
|
150
|
+
@subscribe_lock.synchronize do
|
151
|
+
removed = []
|
152
|
+
ids.each do |id|
|
153
|
+
removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact
|
154
|
+
error "No subscribers with id #{id}" if removed_at_id.empty?
|
155
|
+
removed << removed_at_id
|
156
|
+
end
|
157
|
+
removed.flatten
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Message subscribers. Key is the message class to listen for.
|
162
|
+
# Value is a Hash of subscriber Procs, keyed by their subscription id.
|
163
|
+
# All subscriber Procs will be called with the message instance
|
164
|
+
# as an argument when a message of that type is received.
|
165
|
+
def subscribers
|
166
|
+
@subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new }
|
167
|
+
end
|
168
|
+
|
169
|
+
### Working with received messages Hash
|
170
|
+
|
171
|
+
# Clear received messages Hash
|
172
|
+
def clear_received *message_types
|
173
|
+
@receive_lock.synchronize do
|
174
|
+
if message_types.empty?
|
175
|
+
received.each { |message_type, container| container.clear }
|
176
|
+
else
|
177
|
+
message_types.each { |message_type| received[message_type].clear }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Hash of received messages, keyed by message type
|
183
|
+
def received
|
184
|
+
@received ||= Hash.new { |hash, message_type| hash[message_type] = Array.new }
|
185
|
+
end
|
186
|
+
|
187
|
+
# Check if messages of given type were received at_least n times
|
188
|
+
def received? message_type, times=1
|
189
|
+
@receive_lock.synchronize do
|
190
|
+
received[message_type].size >= times
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Check if all given conditions are satisfied
|
195
|
+
def satisfied? *conditions
|
196
|
+
!conditions.empty? &&
|
197
|
+
conditions.inject(true) do |result, condition|
|
198
|
+
result && if condition.is_a?(Symbol)
|
199
|
+
received?(condition)
|
200
|
+
elsif condition.is_a?(Array)
|
201
|
+
received?(*condition)
|
202
|
+
elsif condition.respond_to?(:call)
|
203
|
+
condition.call
|
204
|
+
else
|
205
|
+
error "Unknown wait condition #{condition}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Wait for specific condition(s) - given as callable/block, or
|
211
|
+
# message type(s) - given as Symbol or [Symbol, times] pair.
|
212
|
+
# Timeout after given time or 1 second.
|
213
|
+
def wait_for *args, &block
|
214
|
+
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
215
|
+
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
216
|
+
conditions = args.delete_if { |arg| arg.is_a? Numeric }.push(block).compact
|
217
|
+
|
218
|
+
until end_time < Time.now || satisfied?(*conditions)
|
219
|
+
if @reader
|
220
|
+
sleep 0.05
|
221
|
+
else
|
222
|
+
process_messages 50
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
### Working with Incoming messages from IB
|
228
|
+
|
229
|
+
# Start reader thread that continuously reads messages from @socket in background.
|
230
|
+
# If you don't start reader, you should manually poll @socket for messages
|
231
|
+
# or use #process_messages(msec) API.
|
232
|
+
def start_reader
|
233
|
+
Thread.abort_on_exception = true
|
234
|
+
@reader_running = true
|
235
|
+
@reader = Thread.new do
|
236
|
+
process_messages while @reader_running
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def reader_running?
|
241
|
+
@reader_running && @reader && @reader.alive?
|
242
|
+
end
|
243
|
+
|
244
|
+
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
245
|
+
def process_messages poll_time = 200 # in msec
|
246
|
+
time_out = Time.now + poll_time/1000.0
|
247
|
+
while (time_left = time_out - Time.now) > 0
|
248
|
+
# If socket is readable, process single incoming message
|
249
|
+
process_message if select [socket], nil, nil, time_left
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Process single incoming message (blocking!)
|
254
|
+
def process_message
|
255
|
+
msg_id = socket.read_int # This read blocks!
|
256
|
+
|
257
|
+
# Debug:
|
258
|
+
log.debug "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"
|
259
|
+
|
260
|
+
# Create new instance of the appropriate message type,
|
261
|
+
# and have it read the message from socket.
|
262
|
+
# NB: Failure here usually means unsupported message type received
|
263
|
+
error "Got unsupported message #{msg_id}" unless Messages::Incoming::Classes[msg_id]
|
264
|
+
msg = Messages::Incoming::Classes[msg_id].new(socket)
|
265
|
+
|
266
|
+
# Deliver message to all registered subscribers, alert if no subscribers
|
267
|
+
@subscribe_lock.synchronize do
|
268
|
+
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
269
|
+
end
|
270
|
+
log.warn "No subscribers for message #{msg.class}!" if subscribers[msg.class].empty?
|
271
|
+
|
272
|
+
# Collect all received messages into a @received Hash
|
273
|
+
if options[:received]
|
274
|
+
@receive_lock.synchronize do
|
275
|
+
received[msg.message_type] << msg
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
### Sending Outgoing messages to IB
|
281
|
+
|
282
|
+
# Send an outgoing message.
|
283
|
+
def send_message what, *args
|
284
|
+
message =
|
285
|
+
case
|
286
|
+
when what.is_a?(Messages::Outgoing::AbstractMessage)
|
287
|
+
what
|
288
|
+
when what.is_a?(Class) && what < Messages::Outgoing::AbstractMessage
|
289
|
+
what.new *args
|
290
|
+
when what.is_a?(Symbol)
|
291
|
+
Messages::Outgoing.const_get(what).new *args
|
292
|
+
|
293
|
+
else
|
294
|
+
error "Only able to send outgoing IB messages", :args
|
295
|
+
end
|
296
|
+
error "Not able to send messages, IB not connected!" unless connected?
|
297
|
+
message.send_to socket
|
298
|
+
end
|
299
|
+
|
300
|
+
alias dispatch send_message # Legacy alias
|
301
|
+
|
302
|
+
# Place Order (convenience wrapper for send_message :PlaceOrder).
|
303
|
+
# Assigns client_id and order_id fields to placed order. Returns assigned order_id.
|
304
|
+
def place_order order, contract
|
305
|
+
order.place contract, self if order.is_a? IB::Order
|
306
|
+
end
|
307
|
+
|
308
|
+
# Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id.
|
309
|
+
def modify_order order, contract
|
310
|
+
order.modify contract, self
|
311
|
+
end
|
312
|
+
|
313
|
+
# Cancel Orders by their local ids (convenience wrapper for send_message :CancelOrder).
|
314
|
+
def cancel_order *local_ids
|
315
|
+
local_ids.each do |local_id|
|
316
|
+
send_message :CancelOrder, :local_id => local_id.to_i
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
protected
|
321
|
+
|
322
|
+
def random_id
|
323
|
+
rand 999999999
|
324
|
+
end
|
325
|
+
|
326
|
+
end # class Connection
|
327
|
+
end # module IB
|