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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ib-api.rb +10 -0
  3. data/lib/ib/base.rb +99 -0
  4. data/lib/ib/base_properties.rb +154 -0
  5. data/lib/ib/connection.rb +327 -0
  6. data/lib/ib/constants.rb +334 -0
  7. data/lib/ib/db.rb +29 -0
  8. data/lib/ib/engine.rb +35 -0
  9. data/lib/ib/errors.rb +40 -0
  10. data/lib/ib/extensions.rb +72 -0
  11. data/lib/ib/flex.rb +106 -0
  12. data/lib/ib/logger.rb +25 -0
  13. data/lib/ib/messages.rb +88 -0
  14. data/lib/ib/messages/abstract_message.rb +89 -0
  15. data/lib/ib/messages/incoming.rb +134 -0
  16. data/lib/ib/messages/incoming/abstract_message.rb +99 -0
  17. data/lib/ib/messages/incoming/alert.rb +34 -0
  18. data/lib/ib/messages/incoming/contract_data.rb +102 -0
  19. data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
  20. data/lib/ib/messages/incoming/execution_data.rb +54 -0
  21. data/lib/ib/messages/incoming/historical_data.rb +55 -0
  22. data/lib/ib/messages/incoming/market_depths.rb +44 -0
  23. data/lib/ib/messages/incoming/next_valid_id.rb +18 -0
  24. data/lib/ib/messages/incoming/open_order.rb +232 -0
  25. data/lib/ib/messages/incoming/order_status.rb +81 -0
  26. data/lib/ib/messages/incoming/portfolio_value.rb +39 -0
  27. data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
  28. data/lib/ib/messages/incoming/scanner_data.rb +53 -0
  29. data/lib/ib/messages/incoming/ticks.rb +131 -0
  30. data/lib/ib/messages/outgoing.rb +331 -0
  31. data/lib/ib/messages/outgoing/abstract_message.rb +73 -0
  32. data/lib/ib/messages/outgoing/bar_requests.rb +189 -0
  33. data/lib/ib/messages/outgoing/place_order.rb +141 -0
  34. data/lib/ib/model.rb +6 -0
  35. data/lib/ib/models.rb +10 -0
  36. data/lib/ib/requires.rb +9 -0
  37. data/lib/ib/socket.rb +81 -0
  38. data/lib/ib/symbols.rb +35 -0
  39. data/lib/ib/symbols/bonds.rb +28 -0
  40. data/lib/ib/symbols/forex.rb +41 -0
  41. data/lib/ib/symbols/futures.rb +117 -0
  42. data/lib/ib/symbols/options.rb +39 -0
  43. data/lib/ib/symbols/stocks.rb +37 -0
  44. data/lib/ib/version.rb +6 -0
  45. data/lib/models/ib/bag.rb +51 -0
  46. data/lib/models/ib/bar.rb +45 -0
  47. data/lib/models/ib/combo_leg.rb +103 -0
  48. data/lib/models/ib/contract.rb +292 -0
  49. data/lib/models/ib/contract_detail.rb +89 -0
  50. data/lib/models/ib/execution.rb +65 -0
  51. data/lib/models/ib/option.rb +60 -0
  52. data/lib/models/ib/order.rb +391 -0
  53. data/lib/models/ib/order_state.rb +128 -0
  54. data/lib/models/ib/underlying.rb +34 -0
  55. metadata +96 -0
@@ -0,0 +1,106 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'xmlsimple'
4
+
5
+ module IB
6
+
7
+ # FLEX is a web-based service from IB that helps you to retrieve your activity,
8
+ # trades and positions. It is working independently from TWS or Gateway, using your
9
+ # internet connection directly. See /misc/flex for extended FLEX documentation.
10
+ #
11
+ # In order to use this service, activate it and configure your token first.
12
+ # Your Token is located at Account Management->Reports->Delivery Settings->Flex Web Service.
13
+ # You need to activate Flex Web Service and generate new token(s) there.
14
+ # Your Flex Query Ids are in Account Management->Reports->Activity->Flex Queries.
15
+ # Create new Flex query and make sure to set its output format to XML.
16
+ #
17
+ # IB::Flex object incapsulates a single pre-defined Flex query.
18
+ class Flex
19
+ class << self
20
+ attr_accessor :token, :uri
21
+
22
+ # By default, uri is a well known FLEX Web Service URI
23
+ def uri
24
+ #@uri || 'https://www.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest'
25
+ @uri || 'https://gdcdyn.interactivebrokers.com/Universal/servlet/FlexStatementService.SendRequest'
26
+ end
27
+ end
28
+
29
+ # Create new Flex query with options:
30
+ # :token => 1111111111111111111111111111111111 # CHANGE to your actual token!
31
+ # :query_id => 11111 # CHANGE to actual query id!
32
+ # :format => :xml (default) / :csv
33
+ # :verbose => true / false (default)
34
+ def initialize opts
35
+ @query_id = opts[:query_id]
36
+ @token = opts[:token] || Flex.token
37
+ @format = opts[:format] || :xml
38
+ @verbose = !!opts[:verbose]
39
+ yield self if block_given?
40
+ end
41
+
42
+ # Run a pre-defined Flex query against IB Flex Web Service
43
+ # Returns a (parsed) report or raises FlexError in case of problems
44
+ def run
45
+ # Initiate FLEX request at a known FLEX Web Service URI
46
+ resp = get_content Flex.uri, :t => @token, :q => @query_id, :v => 3
47
+ error("#{resp['ErrorCode']}: #{resp['ErrorMessage']}", :flex) if resp['Status'] == 'Fail'
48
+
49
+ reference_code = resp['ReferenceCode']
50
+ report_uri = resp['Url']
51
+
52
+ # Retrieve the FLEX report
53
+ report = nil
54
+ until report do
55
+ report = get_content(report_uri, :t => @token, :q => reference_code, :v => 3,
56
+ :text_ok => @format != :xml)
57
+
58
+ # If Status is specified, returned xml contains only error message, not actual report
59
+ if report.is_a?(Hash) && report['Status'] =~ /Fail|Warn/
60
+ error_code = report['ErrorCode'].to_i
61
+ error_message = "#{error_code}: #{report['ErrorMessage']}"
62
+
63
+ case error_code
64
+ when 1001..1009, 1018, 1019, 1021
65
+ # Report is just not ready yet, wait and retry
66
+ puts error_message if @verbose
67
+ report = nil
68
+ sleep 1
69
+ else # Fatal error
70
+ error error_message, :flex
71
+ end
72
+ end
73
+ end
74
+ report
75
+ end
76
+
77
+ # Helper method to get (and parse XML) responses from IB Flex Web Service
78
+ def get_content address, fields
79
+ text_ok = fields.delete(:text_ok)
80
+ resp = get address, fields
81
+ if resp.content_type == 'text/xml'
82
+ XmlSimple.xml_in(resp.body, :ForceArray => false)
83
+ else
84
+ error("Expected xml, got #{resp.content_type}", :flex) unless text_ok
85
+ resp.body
86
+ end
87
+ end
88
+
89
+ # Helper method to get raw responses from IB Flex Web Service
90
+ def get address, fields
91
+ uri = URI("#{address}?" + fields.map { |k, v| "#{k}=#{URI.encode(v.to_s)}" }.join('&'))
92
+
93
+ server = Net::HTTP.new(uri.host, uri.port)
94
+ server.use_ssl = (uri.scheme == 'https')
95
+ server.verify_mode = OpenSSL::SSL::VERIFY_NONE if server.use_ssl? # Avoid OpenSSL failures
96
+
97
+ resp = server.start do |http|
98
+ req = Net::HTTP::Get.new(uri.request_uri)
99
+ http.request(req)
100
+ end
101
+ error("URI responded with #{resp.code}", :flex) unless resp.code.to_i == 200
102
+ resp
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,25 @@
1
+ require "logger"
2
+
3
+ # Add default_logger accessor into Object
4
+ def default_logger
5
+ @default_logger ||= Logger.new(STDOUT).tap do |logger|
6
+ time_format = RUBY_VERSION =~ /1\.8\./ ? '%H:%M:%S.%N' : '%H:%M:%S.%3N'
7
+ logger.formatter = proc do |level, time, prog, msg|
8
+ # "#{time.strftime(time_format)} #{msg}\n"
9
+ "#{time.strftime('%Y-%m-%d %H:%M:%S')} #{msg}\n"
10
+ # Hardcoded 20201204 by PA!
11
+ end
12
+ logger.level = Logger::INFO
13
+ end
14
+ end
15
+
16
+ def default_logger= logger
17
+ @default_logger = logger
18
+ end
19
+
20
+ # Add universally accessible log method/accessor into Object
21
+ def log *args
22
+ default_logger.tap do |logger|
23
+ logger.fatal *args unless args.empty?
24
+ end
25
+ end
@@ -0,0 +1,88 @@
1
+ module IB
2
+ module Messages
3
+ # This gem supports incoming/outgoing IB messages compatible with the following
4
+ # IB client/server versions:
5
+ CLIENT_VERSION = 59 # 59? Maximal client version implemented
6
+ SERVER_VERSION = 62 # 38? 53? 62? Minimal server version required
7
+ end
8
+ end
9
+
10
+ require 'ib/messages/outgoing'
11
+ require 'ib/messages/incoming'
12
+
13
+ __END__
14
+ // Client version history
15
+ //
16
+ // 6 = Added parentId to orderStatus
17
+ // 7 = The new execDetails event returned for an order filled status and reqExecDetails
18
+ // Also market depth is available.
19
+ // 8 = Added lastFillPrice to orderStatus() event and permId to execution details
20
+ // 9 = Added 'averageCost', 'unrealizedPNL', and 'unrealizedPNL' to updatePortfolio event
21
+ // 10 = Added 'serverId' to the 'open order' & 'order status' events.
22
+ // We send back all the API open orders upon connection.
23
+ // Added new methods reqAllOpenOrders, reqAutoOpenOrders()
24
+ // Added FA support - reqExecution has filter.
25
+ // - reqAccountUpdates takes acct code.
26
+ // 11 = Added permId to openOrder event.
27
+ // 12 = requsting open order attributes ignoreRth, hidden, and discretionary
28
+ // 13 = added goodAfterTime
29
+ // 14 = always send size on bid/ask/last tick
30
+ // 15 = send allocation description string on openOrder
31
+ // 16 = can receive account name in account and portfolio updates, and fa params in openOrder
32
+ // 17 = can receive liquidation field in exec reports, and notAutoAvailable field in mkt data
33
+ // 18 = can receive good till date field in open order messages, and request intraday backfill
34
+ // 19 = can receive rthOnly flag in ORDER_STATUS
35
+ // 20 = expects TWS time string on connection after server version >= 20.
36
+ // 21 = can receive bond contract details.
37
+ // 22 = can receive price magnifier in version 2 contract details message
38
+ // 23 = support for scanner
39
+ // 24 = can receive volatility order parameters in open order messages
40
+ // 25 = can receive HMDS query start and end times
41
+ // 26 = can receive option vols in option market data messages
42
+ // 27 = can receive delta neutral order type and delta neutral aux price in place order version 20: API 8.85
43
+ // 28 = can receive option model computation ticks: API 8.9
44
+ // 29 = can receive trail stop limit price in open order and can place them: API 8.91
45
+ // 30 = can receive extended bond contract def, new ticks, and trade count in bars
46
+ // 31 = can receive EFP extensions to scanner and market data, and combo legs on open orders
47
+ // ; can receive RT bars
48
+ // 32 = can receive TickType.LAST_TIMESTAMP
49
+ // ; can receive "whyHeld" in order status messages
50
+ // 33 = can receive ScaleNumComponents and ScaleComponentSize is open order messages
51
+ // 34 = can receive whatIf orders / order state
52
+ // 35 = can receive contId field for Contract objects
53
+ // 36 = can receive outsideRth field for Order objects
54
+ // 37 = can receive clearingAccount and clearingIntent for Order objects
55
+ // 38 = can receive multiplier and primaryExchange in portfolio updates
56
+ // ; can receive cumQty and avgPrice in execution
57
+ // ; can receive fundamental data
58
+ // ; can receive underComp for Contract objects
59
+ // ; can receive reqId and end marker in contractDetails/bondContractDetails
60
+ // ; can receive ScaleInitComponentSize and ScaleSubsComponentSize for Order objects
61
+ // 39 = can receive underConId in contractDetails
62
+ // 40 = can receive algoStrategy/algoParams in openOrder
63
+ // 41 = can receive end marker for openOrder
64
+ // ; can receive end marker for account download
65
+ // ; can receive end marker for executions download
66
+ // 42 = can receive deltaNeutralValidation
67
+ // 43 = can receive longName(companyName)
68
+ // ; can receive listingExchange
69
+ // ; can receive RTVolume tick
70
+ // 44 = can receive end market for ticker snapshot
71
+ // 45 = can receive notHeld field in openOrder
72
+ // 46 = can receive contractMonth, industry, category, subcategory fields in contractDetails
73
+ // ; can receive timeZoneId, tradingHours, liquidHours fields in contractDetails
74
+ // 47 = can receive gamma, vega, theta, undPrice fields in TICK_OPTION_COMPUTATION
75
+ // 48 = can receive exemptCode in openOrder
76
+ // 49 = can receive hedgeType and hedgeParam in openOrder
77
+ // 50 = can receive optOutSmartRouting field in openOrder
78
+ // 51 = can receive smartComboRoutingParams in openOrder
79
+ // 52 = can receive deltaNeutralConId, deltaNeutralSettlingFirm, deltaNeutralClearingAccount and deltaNeutralClearingIntent in openOrder
80
+ // 53 = can receive orderRef in execution
81
+ // 54 = can receive scale order fields (PriceAdjustValue, PriceAdjustInterval, ProfitOffset, AutoReset,
82
+ // InitPosition, InitFillQty and RandomPercent) in openOrder
83
+ // 55 = can receive orderComboLegs (price) in openOrder
84
+ // 56 = can receive trailingPercent in openOrder
85
+ // 57 = can receive commissionReport message
86
+ // 58 = can receive CUSIP/ISIN/etc. in contractDescription/bondContractDescription
87
+ // 59 = can receive evRule, evMultiplier in contractDescription/bondContractDescription/executionDetails
88
+ // can receive multiplier in executionDetails
@@ -0,0 +1,89 @@
1
+ module IB
2
+ module Messages
3
+
4
+ # This is just a basic generic message from the server.
5
+ #
6
+ # Class variables:
7
+ # @message_id - int: message id.
8
+ # @message_type - Symbol: message type (e.g. :OpenOrderEnd)
9
+ #
10
+ # Instance attributes (at least):
11
+ # @version - int: current version of message format.
12
+ # @data - Hash of actual data read from a stream.
13
+ class AbstractMessage
14
+
15
+ # Class methods
16
+ def self.data_map # Map for converting between structured message and raw data
17
+ @data_map ||= []
18
+ end
19
+
20
+ def self.version # Per class, minimum message version supported
21
+ @version || 1
22
+ end
23
+
24
+ def self.message_id
25
+ @message_id
26
+ end
27
+
28
+ # Returns message type Symbol (e.g. :OpenOrderEnd)
29
+ def self.message_type
30
+ to_s.split(/::/).last.to_sym
31
+ end
32
+
33
+ def message_id
34
+ self.class.message_id
35
+ end
36
+
37
+ def message_type
38
+ self.class.message_type
39
+ end
40
+
41
+ attr_accessor :created_at, :data
42
+
43
+ def to_human
44
+ "<#{self.message_type}:" +
45
+ @data.map do |key, value|
46
+ unless [:version].include?(key)
47
+ " #{key} #{ value.is_a?(Hash) ? value.inspect : value}"
48
+ end
49
+ end.compact.join(',') + " >"
50
+ end
51
+
52
+ end # class AbstractMessage
53
+
54
+ # Macro that defines short message classes using a one-liner.
55
+ # First arg is either a [message_id, version] pair or just message_id (version 1)
56
+ # data_map contains instructions for processing @data Hash. Format:
57
+ # Incoming messages: [field, type] or [group, field, type]
58
+ # Outgoing messages: field, [field, default] or [field, method, [args]]
59
+ def def_message message_id_version, *data_map, &to_human
60
+ base = data_map.first.is_a?(Class) ? data_map.shift : self::AbstractMessage
61
+ message_id, version = message_id_version
62
+
63
+ # Define new message class
64
+ message_class = Class.new(base) do
65
+ @message_id, @version = message_id, version || 1
66
+ @data_map = data_map
67
+
68
+ @data_map.each do |(name, _, type_args)|
69
+ # Avoid redefining existing accessor methods
70
+ unless instance_methods.include?(name.to_s) || instance_methods.include?(name.to_sym)
71
+ if type_args.is_a?(Symbol) # This is Incoming with [group, field, type]
72
+ attr_reader name
73
+ else
74
+ define_method(name) { @data[name] }
75
+ end
76
+ end
77
+ end
78
+
79
+ define_method(:to_human, &to_human) if to_human
80
+ end
81
+
82
+ # Add defined message class to Classes Hash keyed by its message_id
83
+ self::Classes[message_id] = message_class
84
+
85
+ message_class
86
+ end
87
+
88
+ end # module Messages
89
+ end # module IB
@@ -0,0 +1,134 @@
1
+ require 'ib/messages/incoming/abstract_message'
2
+
3
+ # EClientSocket.java uses sendMax() rather than send() for a number of these.
4
+ # It sends an EOL rather than a number if the value == Integer.MAX_VALUE (or Double.MAX_VALUE).
5
+ # These fields are initialized to this MAX_VALUE.
6
+ # This has been implemented with nils in Ruby to represent the case where an EOL should be sent.
7
+
8
+ # TODO: Don't instantiate messages, use their classes as just namespace for .encode/decode
9
+ # TODO: realize Message#fire method that raises EWrapper events
10
+
11
+ module IB
12
+ module Messages
13
+
14
+ # Incoming IB messages (received from TWS/Gateway)
15
+ module Incoming
16
+ extend Messages # def_message macros
17
+
18
+ ### Define short message classes in-line:
19
+
20
+ AccountValue = def_message([6, 2], [:key, :string],
21
+ [:value, :string],
22
+ [:currency, :string],
23
+ [:account_name, :string]) do
24
+ "<AccountValue: #{account_name}, #{key}=#{value} #{currency}>"
25
+ end
26
+
27
+ AccountUpdateTime = def_message 8, [:time_stamp, :string]
28
+
29
+ NewsBulletins =
30
+ def_message 14, [:request_id, :int], # unique incrementing bulletin ID.
31
+ [:type, :int], # Type of bulletin. Valid values include:
32
+ # 1 = Regular news bulletin
33
+ # 2 = Exchange no longer available for trading
34
+ # 3 = Exchange is available for trading
35
+ [:text, :string], # The bulletin's message text.
36
+ [:exchange, :string] # Exchange from which this message originated.
37
+
38
+ ManagedAccounts =
39
+ def_message 15, [:accounts_list, :string]
40
+
41
+ # Receives previously requested FA configuration information from TWS.
42
+ ReceiveFA =
43
+ def_message 16, [:type, :int], # type of Financial Advisor configuration data
44
+ # being received from TWS. Valid values include:
45
+ # 1 = GROUPS, 2 = PROFILE, 3 = ACCOUNT ALIASES
46
+ [:xml, :string] # XML string with requested FA configuration information.
47
+
48
+ # Receives an XML document that describes the valid parameters that a scanner
49
+ # subscription can have (for outgoing RequestScannerSubscription message).
50
+ ScannerParameters = def_message 19, [:xml, :string]
51
+
52
+ # Receives the current system time on the server side.
53
+ CurrentTime = def_message 49, [:time, :int] # long!
54
+
55
+ # Receive Reuters global fundamental market data. There must be a subscription to
56
+ # Reuters Fundamental set up in Account Management before you can receive this data.
57
+ FundamentalData = def_message 51, [:request_id, :int], [:xml, :string]
58
+
59
+ ContractDataEnd = def_message 52, [:request_id, :int]
60
+
61
+ OpenOrderEnd = def_message 53
62
+
63
+ AccountDownloadEnd = def_message 54, [:account_name, :string]
64
+
65
+ ExecutionDataEnd = def_message 55, [:request_id, :int]
66
+
67
+ MarketDataType = def_message 58, [:request_id, :int], [:market_data_type, :int]
68
+
69
+ CommissionReport =
70
+ def_message 59, [:exec_id, :string],
71
+ [:commission, :decimal], # Commission amount.
72
+ [:currency, :string], # Commission currency
73
+ [:realized_pnl, :decimal_max],
74
+ [:yield, :decimal_max],
75
+ [:yield_redemption_date, :int] # YYYYMMDD format
76
+
77
+ ### Require standalone source files for more complex message classes:
78
+
79
+ require 'ib/messages/incoming/alert'
80
+ require 'ib/messages/incoming/contract_data'
81
+ require 'ib/messages/incoming/delta_neutral_validation'
82
+ require 'ib/messages/incoming/execution_data'
83
+ require 'ib/messages/incoming/historical_data'
84
+ require 'ib/messages/incoming/market_depths'
85
+ require 'ib/messages/incoming/next_valid_id'
86
+ require 'ib/messages/incoming/open_order'
87
+ require 'ib/messages/incoming/order_status'
88
+ require 'ib/messages/incoming/portfolio_value'
89
+ require 'ib/messages/incoming/real_time_bar'
90
+ require 'ib/messages/incoming/scanner_data'
91
+ require 'ib/messages/incoming/ticks'
92
+
93
+ end # module Incoming
94
+ end # module Messages
95
+ end # module IB
96
+
97
+
98
+ __END__
99
+ // incoming msg id's
100
+ static final int TICK_PRICE = 1; *
101
+ static final int TICK_SIZE = 2; *
102
+ static final int ORDER_STATUS = 3; *
103
+ static final int ERR_MSG = 4; *
104
+ static final int OPEN_ORDER = 5; *
105
+ static final int ACCT_VALUE = 6; *
106
+ static final int PORTFOLIO_VALUE = 7; *
107
+ static final int ACCT_UPDATE_TIME = 8; *
108
+ static final int NEXT_VALID_ID = 9; *
109
+ static final int CONTRACT_DATA = 10; *
110
+ static final int EXECUTION_DATA = 11; ?
111
+ static final int MARKET_DEPTH = 12; *
112
+ static final int MARKET_DEPTH_L2 = 13; *
113
+ static final int NEWS_BULLETINS = 14; *
114
+ static final int MANAGED_ACCTS = 15; *
115
+ static final int RECEIVE_FA = 16; *
116
+ static final int HISTORICAL_DATA = 17; *
117
+ static final int BOND_CONTRACT_DATA = 18; *
118
+ static final int SCANNER_PARAMETERS = 19; *
119
+ static final int SCANNER_DATA = 20; *
120
+ static final int TICK_OPTION_COMPUTATION = 21; *
121
+ static final int TICK_GENERIC = 45; *
122
+ static final int TICK_STRING = 46; *
123
+ static final int TICK_EFP = 47; *
124
+ static final int CURRENT_TIME = 49; *
125
+ static final int REAL_TIME_BARS = 50; *
126
+ static final int FUNDAMENTAL_DATA = 51; *
127
+ static final int CONTRACT_DATA_END = 52; *
128
+ static final int OPEN_ORDER_END = 53; *
129
+ static final int ACCT_DOWNLOAD_END = 54; *
130
+ static final int EXECUTION_DATA_END = 55; *
131
+ static final int DELTA_NEUTRAL_VALIDATION = 56; *
132
+ static final int TICK_SNAPSHOT_END = 57; *
133
+ static final int MARKET_DATA_TYPE = 58; ?
134
+ static final int COMMISSION_REPORT = 59; ?
@@ -0,0 +1,99 @@
1
+ require 'ib/messages/abstract_message'
2
+
3
+ module IB
4
+ module Messages
5
+ module Incoming
6
+
7
+ # Container for specific message classes, keyed by their message_ids
8
+ Classes = {}
9
+
10
+ class AbstractMessage < IB::Messages::AbstractMessage
11
+
12
+ attr_accessor :socket
13
+
14
+ def version # Per message, received messages may have the different versions
15
+ @data[:version]
16
+ end
17
+
18
+ def check_version actual, expected
19
+ unless actual == expected || expected.is_a?(Array) && expected.include?(actual)
20
+ error "Unsupported version #{actual} received, expected #{expected}"
21
+ end
22
+ end
23
+
24
+ # Create incoming message from a given source (IB Socket or data Hash)
25
+ def initialize source
26
+ @created_at = Time.now
27
+ if source.is_a?(Hash) # Source is a @data Hash
28
+ @data = source
29
+ else # Source is a Socket
30
+ @socket = source
31
+ @data = Hash.new
32
+ self.load
33
+ end
34
+ end
35
+
36
+ # Every message loads received message version first
37
+ # Override the load method in your subclass to do actual reading into @data.
38
+ def load
39
+ if socket
40
+ @data[:version] = socket.read_int
41
+
42
+ check_version @data[:version], self.class.version
43
+
44
+ load_map *self.class.data_map
45
+ else
46
+ raise "Unable to load, no socket"
47
+ end
48
+
49
+ rescue => e
50
+ error "Reading #{self.class}: #{e.class}: #{e.message}", :load, e.backtrace
51
+ end
52
+
53
+ # Load @data from the socket according to the given data map.
54
+ #
55
+ # map is a series of Arrays in the format of
56
+ # [ :name, :type ], [ :group, :name, :type]
57
+ # type identifiers must have a corresponding read_type method on socket (read_int, etc.).
58
+ # group is used to lump together aggregates, such as Contract or Order fields
59
+ def load_map(*map)
60
+ map.each do |instruction|
61
+ # We determine the function of the first element
62
+ head = instruction.first
63
+ case head
64
+ when Integer # >= Version condition: [ min_version, [map]]
65
+ load_map *instruction.drop(1) if version >= head
66
+
67
+ when Proc # Callable condition: [ condition, [map]]
68
+ load_map *instruction.drop(1) if head.call
69
+
70
+ when true # Pre-condition already succeeded!
71
+ load_map *instruction.drop(1)
72
+
73
+ when nil, false # Pre-condition already failed! Do nothing...
74
+
75
+ when Symbol # Normal map
76
+ group, name, type, block =
77
+ if instruction[2].nil? || instruction[2].is_a?(Proc)
78
+ [nil] + instruction # No group, [ :name, :type, (:block) ]
79
+ else
80
+ instruction # [ :group, :name, :type, (:block)]
81
+ end
82
+
83
+ data = socket.__send__("read_#{type}", &block)
84
+ if group
85
+ @data[group] ||= {}
86
+ @data[group][name] = data
87
+ else
88
+ @data[name] = data
89
+ end
90
+ else
91
+ error "Unrecognized instruction #{instruction}"
92
+ end
93
+ end
94
+ end
95
+
96
+ end # class AbstractMessage
97
+ end # module Incoming
98
+ end # module Messages
99
+ end # module IB