wdevauld-ib-ruby 0.2

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.
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env ruby -w
2
+ #
3
+ # Copyright (C) 2007 Paul Legato.
4
+ #
5
+ # This library is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as
7
+ # published by the Free Software Foundation; either version 2.1 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful, but
11
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
+ # 02110-1301 USA
19
+ #
20
+
21
+ $:.push(File.dirname(__FILE__) + "/../")
22
+
23
+ require 'ib'
24
+ require 'datatypes'
25
+ require 'symbols/forex'
26
+
27
+ #
28
+ # Definition of what we want market data for. We have to keep track
29
+ # of what ticker id corresponds to what symbol ourselves, because the
30
+ # ticks don't include any other identifying information.
31
+ #
32
+ # The choice of ticker ids is, as far as I can tell, arbitrary.
33
+ #
34
+ @market =
35
+ {
36
+ 123 => IB::Symbols::Forex[:gbpusd],
37
+ 456 => IB::Symbols::Forex[:eurusd]
38
+ }
39
+
40
+
41
+ # First, connect to IB TWS.
42
+ ib = IB::IB.new
43
+
44
+
45
+ #
46
+ # Now, subscribe to TickerPrice and TickerSize events. The code
47
+ # passed in the block will be executed when a message of that type is
48
+ # received, with the received message as its argument. In this case,
49
+ # we just print out the tick.
50
+ #
51
+ # Note that we have to look the ticker id of each incoming message
52
+ # up in local memory to figure out what it's for.
53
+ #
54
+ # (N.B. The description field is not from IB TWS. It is defined
55
+ # locally in forex.rb, and is just arbitrary text.)
56
+
57
+ ib.subscribe(IB::IncomingMessages::TickPrice, lambda {|msg|
58
+ puts @market[msg.data[:ticker_id]].description + ": " + msg.to_human
59
+ })
60
+
61
+ ib.subscribe(IB::IncomingMessages::TickSize, lambda {|msg|
62
+ puts @market[msg.data[:ticker_id]].description + ": " + msg.to_human
63
+ })
64
+
65
+
66
+ # Now we actually request market data for the symbols we're interested in.
67
+
68
+ @market.each_pair {|id, contract|
69
+ msg = IB::OutgoingMessages::RequestMarketData.new({
70
+ :ticker_id => id,
71
+ :contract => contract
72
+ })
73
+ ib.dispatch(msg)
74
+ }
75
+
76
+
77
+ puts "Main thread going to sleep. Press ^C to quit.."
78
+ while true
79
+ sleep 2
80
+ end
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby -w
2
+ #
3
+ # Copyright (C) 2007 Paul Legato.
4
+ #
5
+ # This library is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU Lesser General Public License as
7
+ # published by the Free Software Foundation; either version 2.1 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This library is distributed in the hope that it will be useful, but
11
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
+ # Lesser General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public
16
+ # License along with this library; if not, write to the Free Software
17
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18
+ # 02110-1301 USA
19
+ #
20
+
21
+ $:.push(File.dirname(__FILE__) + "/../")
22
+
23
+ require 'ib'
24
+ require 'datatypes'
25
+ require 'symbols/futures'
26
+
27
+ # First, connect to IB TWS.
28
+ ib = IB::IB.new
29
+
30
+ # Uncomment this for verbose debug messages:
31
+ # IB::IBLogger.level = Logger::Severity::DEBUG
32
+
33
+ # Define the symbols we're interested in.
34
+ @market =
35
+ {
36
+ 123 => IB::Symbols::Futures[:gbp],
37
+ 234 => IB::Symbols::Futures[:jpy]
38
+ }
39
+
40
+
41
+ # This method filters out non-:last type events, and filters out any
42
+ # sale < MIN_SIZE.
43
+ MIN_SIZE = 0
44
+
45
+ def showSales(msg)
46
+ return if msg.data[:type] != :last || msg.data[:size] < MIN_SIZE
47
+ puts @market[msg.data[:ticker_id]].description + ": " + msg.data[:size].to_s + " at " + msg.data[:price].to_digits
48
+ end
49
+
50
+ def showSize(msg)
51
+ puts @market[msg.data[:ticker_id]].description + ": " + msg.to_human
52
+ end
53
+
54
+
55
+ #
56
+ # Now, subscribe to TickerPrice and TickerSize events. The code
57
+ # passed in the block will be executed when a message of that type is
58
+ # received, with the received message as its argument. In this case,
59
+ # we just print out the tick.
60
+ #
61
+ # Note that we have to look the ticker id of each incoming message
62
+ # up in local memory to figure out what it's for.
63
+ #
64
+ # (N.B. The description field is not from IB TWS. It is defined
65
+ # locally in forex.rb, and is just arbitrary text.)
66
+
67
+ ib.subscribe(IB::IncomingMessages::TickPrice, lambda {|msg|
68
+ showSales(msg)
69
+ })
70
+
71
+ ib.subscribe(IB::IncomingMessages::TickSize, lambda {|msg|
72
+ showSize(msg)
73
+ })
74
+
75
+
76
+ # Now we actually request market data for the symbols we're interested in.
77
+
78
+ @market.each_pair {|id, contract|
79
+ msg = IB::OutgoingMessages::RequestMarketData.new({
80
+ :ticker_id => id,
81
+ :contract => contract
82
+ })
83
+ ib.dispatch(msg)
84
+ }
85
+
86
+
87
+ puts "\n\n\t******** Press <Enter> to quit.. *********\n\n"
88
+
89
+ gets
90
+
91
+ puts "Unsubscribing from TWS market data.."
92
+
93
+ @market.each_pair {|id, contract|
94
+ msg = IB::OutgoingMessages::CancelMarketData.new({
95
+ :ticker_id => id,
96
+ })
97
+ ib.dispatch(msg)
98
+ }
99
+
100
+ puts "Done."
101
+
data/bin/ib-ruby ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib ib-ruby]))
5
+
6
+ # Put your code here
7
+
8
+ # EOF
data/lib/ib-ruby.rb ADDED
@@ -0,0 +1,49 @@
1
+
2
+ module IbRuby
3
+
4
+ # :stopdoc:
5
+ VERSION = '1.0.0'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ # Utility method used to rquire all files ending in .rb that lie in the
33
+ # directory below this file that has the same name as the filename passed
34
+ # in. Optionally, a specific _directory_ name can be passed in such that
35
+ # the _filename_ does not have to be equivalent to the directory.
36
+ #
37
+ def self.require_all_libs_relative_to( fname, dir = nil )
38
+ dir ||= ::File.basename(fname, '.*')
39
+ search_me = ::File.expand_path(
40
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41
+
42
+ Dir.glob(search_me).sort.each {|rb| require rb}
43
+ end
44
+
45
+ end # module IbRuby
46
+
47
+ IbRuby.require_all_libs_relative_to(__FILE__)
48
+
49
+ # EOF
@@ -0,0 +1,402 @@
1
+ #
2
+ # Copyright (C) 2006 Blue Voodoo Magic LLC.
3
+ #
4
+ # This library is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU Lesser General Public License as
6
+ # published by the Free Software Foundation; either version 2.1 of the
7
+ # License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful, but
10
+ # WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17
+ # 02110-1301 USA
18
+ #
19
+
20
+ #
21
+ # TODO: Implement equals() according to the criteria in IB's Java client.
22
+ #
23
+
24
+ require 'Logger'
25
+
26
+ module IB
27
+
28
+ logger = Logger.new(STDERR)
29
+
30
+ module Datatypes
31
+ attr_reader :created_at
32
+
33
+ class AbstractDatum
34
+ def init
35
+ @created_at = Time.now
36
+ end
37
+
38
+ # If a hash is given, keys are taken as attribute names, values as data.
39
+ # The attrs of the instance are set automatically from the attributeHash.
40
+ #
41
+ # If no hash is given, #init is called in the instance. #init
42
+ # should set the datum up in a generic state.
43
+ #
44
+ def initialize(attributeHash=nil)
45
+ if attributeHash.nil?
46
+ init
47
+ else
48
+ raise(ArgumentError.new("Argument must be a Hash")) unless attributeHash.is_a?(Hash)
49
+ attributeHash.keys.each {|key|
50
+ self.send((key.to_s + "=").to_sym, attributeHash[key])
51
+ }
52
+ end
53
+ end
54
+ end # AbstractDatum
55
+
56
+
57
+ # This is used within HistoricData messages.
58
+ # Instantiate with a Hash of attributes, to be auto-set via initialize in AbstractDatum.
59
+ class Bar < AbstractDatum
60
+ attr_accessor :date, :open, :high, :low, :close, :volume, :wap, :has_gaps
61
+
62
+ def to_s
63
+ "<Bar: #{@date}; OHLC: #{@open.to_digits}, #{@high.to_digits}, #{@low.to_digits}, #{@close.to_digits}; volume: #{@volume}; wap: #{@wap.to_digits}; has_gaps: #{@has_gaps}>"
64
+ end
65
+
66
+ end # Bar
67
+
68
+
69
+ class Order < AbstractDatum
70
+ # Constants used in Order objects. Drawn from Order.java
71
+ Origin_Customer = 0
72
+ Origin_Firm = 1
73
+
74
+ Opt_Unknown = '?'
75
+ Opt_Broker_Dealer = 'b'
76
+ Opt_Customer = 'c'
77
+ Opt_Firm = 'f'
78
+ Opt_Isemm = 'm'
79
+ Opt_Farmm = 'n'
80
+ Opt_Specialist = 'y'
81
+
82
+ # Main order fields
83
+ attr_accessor(:id, :client_id, :perm_id, :action, :total_quantity, :order_type, :limit_price,
84
+ :aux_price, :shares_allocation)
85
+
86
+ # Extended order fields
87
+ attr_accessor(:tif, :oca_group, :account, :open_close, :origin, :order_ref,
88
+ :transmit, # if false, order will be created but not transmitted.
89
+ :parent_id, # Parent order id, to associate auto STP or TRAIL orders with the original order.
90
+ :block_order,
91
+ :sweep_to_fill,
92
+ :display_size,
93
+ :trigger_method,
94
+ :ignore_rth,
95
+ :hidden,
96
+ :discretionary_amount,
97
+ :good_after_time,
98
+ :good_till_date)
99
+
100
+ OCA_Cancel_with_block = 1
101
+ OCA_Reduce_with_block = 2
102
+ OCA_Reduce_non_block = 3
103
+
104
+ # No idea what the fa_* attributes are for, nor many of the others.
105
+ attr_accessor(:fa_group, :fa_profile, :fa_method, :fa_profile, :fa_method, :fa_percentage, :primary_exchange,
106
+ :short_sale_slot, # 1 or 2, says Order.java. (No idea what the difference is.)
107
+ :designated_location, # "when slot=2 only"
108
+ :oca_type, # 1 = CANCEL_WITH_BLOCK, 2 = REDUCE_WITH_BLOCK, 3 = REDUCE_NON_BLOCK
109
+ :rth_only, :override_percentage_constraints, :rule_80a, :settling_firm, :all_or_none,
110
+ :min_quantity, :percent_offset, :etrade_only, :firm_quote_only, :nbbo_price_cap)
111
+
112
+ # Box orders only:
113
+ Box_Auction_Match = 1
114
+ Box_Auction_Improvement = 2
115
+ Box_Auction_Transparent = 3
116
+ attr_accessor(:auction_strategy, # Box_* constants above
117
+ :starting_price, :stock_ref_price, :delta, :stock_range_lower, :stock_range_upper)
118
+
119
+ # Volatility orders only:
120
+ Volatility_Type_Daily = 1
121
+ Volatility_Type_Annual = 2
122
+
123
+ Volatility_Ref_Price_Average = 1
124
+ Volatility_Ref_Price_BidOrAsk = 2
125
+
126
+ attr_accessor(:volatility,
127
+ :volatility_type, # 1 = daily, 2 = annual, as above
128
+ :continuous_update,
129
+ :reference_price_type, # 1 = average, 2 = BidOrAsk
130
+ :delta_neutral_order_type,
131
+ :delta_neutral_aux_price)
132
+
133
+ Max_value = 99999999 # I don't know why IB uses a very large number as the default for certain fields
134
+ def init
135
+ super
136
+
137
+ @open_close = "0"
138
+ @origin = Origin_Customer
139
+ @transmit = true
140
+ @primary_exchange = ''
141
+ @designated_location = ''
142
+ @min_quantity = Max_value
143
+ @percent_offset = Max_value
144
+ @nbba_price_cap = Max_value
145
+ @starting_price = Max_value
146
+ @stock_ref_price = Max_value
147
+ @delta = Max_value
148
+ @delta_neutral_order_type = ''
149
+ @delta_neutral_aux_price = Max_value
150
+ @reference_price_type = Max_value
151
+ end # init
152
+
153
+ end # class Order
154
+
155
+
156
+ class Contract < AbstractDatum
157
+
158
+ # Valid security types (sec_type attribute)
159
+ SECURITY_TYPES =
160
+ {
161
+ :stock => "STK",
162
+ :option => "OPT",
163
+ :future => "FUT",
164
+ :index => "IND",
165
+ :futures_option => "FOP",
166
+ :forex => "CASH",
167
+ :bag => "BAG"
168
+ }
169
+
170
+ # note that the :description field is entirely local to ib-ruby, and not part of TWS.
171
+ # You can use it to store whatever arbitrary data you want.
172
+
173
+ attr_accessor(:symbol, :strike, :multiplier, :exchange, :currency,
174
+ :local_symbol, :combo_legs, :description)
175
+
176
+ # Bond values
177
+ attr_accessor(:cusip, :ratings, :desc_append, :bond_type, :coupon_type, :callable, :puttable,
178
+ :coupon, :convertible, :maturity, :issue_date)
179
+
180
+ attr_reader :sec_type, :expiry, :right, :primary_exchange
181
+
182
+
183
+
184
+ # some protective filters
185
+
186
+ def primary_exchange=(x)
187
+ x.upcase! if x.is_a?(String)
188
+
189
+ # per http://chuckcaplan.com/twsapi/index.php/Class%20Contract
190
+ raise(ArgumentError.new("Don't set primary_exchange to smart")) if x == "SMART"
191
+
192
+ @primary_exchange = x
193
+ end
194
+
195
+ def right=(x)
196
+ x.upcase! if x.is_a?(String)
197
+ x = nil if !x.nil? && x.empty?
198
+ raise(ArgumentError.new("Invalid right \"#{x}\" (must be one of PUT, CALL, P, C)")) unless x.nil? || [ "PUT", "CALL", "P", "C"].include?(x)
199
+ @right = x
200
+ end
201
+
202
+ def expiry=(x)
203
+ x = nil if !x.nil? && x.empty?
204
+ raise(ArgumentError.new("Invalid expiry \"#{x}\" (must be in format YYYYMM or YYYYMMDD)")) unless x.nil? || x.to_s =~ /^\d\d\d\d\d\d(\d\d)?$/
205
+ @expiry = x.to_s
206
+ end
207
+
208
+ def sec_type=(x)
209
+ x = nil if !x.nil? && x.empty?
210
+ raise(ArgumentError.new("Invalid security type \"#{x}\" (see SECURITY_TYPES constant in Contract class for valid types)")) unless x.nil? || SECURITY_TYPES.values.include?(x)
211
+ @sec_type = x
212
+ end
213
+
214
+ def reset
215
+ @combo_legs = Array.new
216
+ @strike = 0
217
+ end
218
+
219
+ # Different messages serialize contracts differently. Go figure.
220
+ def serialize_short(version)
221
+ q = [ self.symbol,
222
+ self.sec_type,
223
+ self.expiry,
224
+ self.strike,
225
+ self.right ]
226
+
227
+ q.push(self.multiplier) if version >= 15
228
+ q.concat([
229
+ self.exchange,
230
+ self.currency,
231
+ self.local_symbol
232
+ ])
233
+
234
+ q
235
+ end # serialize
236
+
237
+ # This returns an Array of data from the given contract, in standard format.
238
+ # Note that it does not include the combo legs.
239
+ def serialize_long(version)
240
+ queue = [
241
+ self.symbol,
242
+ self.sec_type,
243
+ self.expiry,
244
+ self.strike,
245
+ self.right
246
+ ]
247
+
248
+ queue.push(self.multiplier) if version >= 15
249
+ queue.push(self.exchange)
250
+ queue.push(self.primary_exchange) if version >= 14
251
+ queue.push(self.currency)
252
+ queue.push(self.local_symbol) if version >= 2
253
+
254
+ queue
255
+ end # serialize_long
256
+
257
+ #
258
+ # This produces a string uniquely identifying this contract, in the format used
259
+ # for command line arguments in the IB-Ruby examples. The format is:
260
+ #
261
+ # symbol:security_type:expiry:strike:right:multiplier:exchange:primary_exchange:currency:local_symbol
262
+ #
263
+ # Fields not needed for a particular security should be left blank (e.g. strike and right are only relevant for options.)
264
+ #
265
+ # For example, to query the British pound futures contract trading on Globex expiring in September, 2008,
266
+ # the string is:
267
+ #
268
+ # GBP:FUT:200809:::62500:GLOBEX::USD:
269
+ #
270
+
271
+ def serialize_ib_ruby(version)
272
+ serialize_long(version).join(":")
273
+ end
274
+
275
+ # This returns a Contract initialized from the serialize_ib_ruby format string.
276
+ def self.from_ib_ruby(string)
277
+ c = Contract.new
278
+ c.symbol, c.sec_type, c.expiry, c.strike, c.right, c.multiplier, c.exchange, c.primary_exchange, c.currency, c.local_symbol = string.split(":")
279
+
280
+ c
281
+ end
282
+
283
+ # Some messages send open_close too, some don't. WTF.
284
+ def serialize_combo_legs(include_open_close = false)
285
+ if self.combo_legs.nil?
286
+ [0]
287
+ else
288
+ [ self.combo_legs.size ].concat(self.combo_legs.serialize(include_open_close))
289
+ end
290
+ end
291
+
292
+ def init
293
+ super
294
+
295
+ @combo_legs = Array.new
296
+ @strike = 0
297
+ @sec_type = ''
298
+ end
299
+
300
+ def to_human
301
+ "<IB-Contract: " + [symbol, expiry, sec_type, strike, right, exchange, currency].join("-") + "}>"
302
+ end
303
+
304
+ def to_short
305
+ "#{symbol}#{expiry}#{strike}#{right}#{exchange}#{currency}"
306
+ end
307
+
308
+ def to_s
309
+ to_human
310
+ end
311
+
312
+ end # class Contract
313
+
314
+
315
+ class ContractDetails < AbstractDatum
316
+ attr_accessor :summary, :market_name, :trading_class, :con_id, :min_tick, :multiplier, :price_magnifier, :order_types, :valid_exchanges
317
+
318
+ def init
319
+ super
320
+
321
+ @summary = Contract.new
322
+ @con_id = 0
323
+ @min_tick = 0
324
+ end
325
+ end # class ContractDetails
326
+
327
+
328
+ class Execution < AbstractDatum
329
+ attr_accessor :order_id, :client_id, :exec_id, :time, :account_number, :exchange, :side, :shares, :price, :perm_id, :liquidation
330
+
331
+ def init
332
+ super
333
+
334
+ @order_id = 0
335
+ @client_id = 0
336
+ @shares = 0
337
+ @price = 0
338
+ @perm_id = 0
339
+ @liquidation =0
340
+ end
341
+ end # Execution
342
+
343
+ # EClientSocket.java tells us: 'Note that the valid format for m_time is "yyyymmdd-hh:mm:ss"'
344
+ class ExecutionFilter < AbstractDatum
345
+ attr_accessor :client_id, :acct_code, :time, :symbol, :sec_type, :exchange, :side
346
+
347
+ def init
348
+ super
349
+
350
+ @client_id = 0
351
+ end
352
+
353
+ end # ExecutionFilter
354
+
355
+
356
+ class ComboLeg < AbstractDatum
357
+ attr_accessor :con_id, :ratio, :action, :exchange, :open_close
358
+
359
+ def init
360
+ super
361
+
362
+ @con_id = 0
363
+ @ratio = 0
364
+ @open_close = 0
365
+ end
366
+
367
+ # Some messages include open_close, some don't. wtf.
368
+ def serialize(include_open_close = false)
369
+ self.collect { |leg|
370
+ [ leg.con_id, leg.ratio, leg.action, leg.exchange, (include_open_close ? leg.open_close : [] )]
371
+ }.flatten
372
+ end
373
+ end # ComboLeg
374
+
375
+
376
+ class ScannerSubscription < AbstractDatum
377
+ attr_accessor :number_of_rows, :instrument, :location_code, :scan_code, :above_price, :below_price,
378
+ :above_volume, :average_option_volume_above, :market_cap_above, :market_cap_below, :moody_rating_above,
379
+ :moody_rating_below, :sp_rating_above, :sp_rating_below, :maturity_date_above, :maturity_date_below,
380
+ :coupon_rate_above, :coupon_rate_below, :exclude_convertible, :scanner_setting_pairs, :stock_type_filter
381
+
382
+ def init
383
+ super
384
+
385
+ @coupon_rate_above = @coupon_rate_below = @market_cap_below = @market_cap_above = @average_option_volume_above =
386
+ @above_volume = @below_price = @above_price = nil
387
+ @number_of_rows = -1 # none specified, per ScannerSubscription.java
388
+ end
389
+ end # ScannerSubscription
390
+
391
+
392
+ # Just like a Hash, but throws an exception if you try to access a key that doesn't exist.
393
+ class StringentHash < Hash
394
+ def initialize(hash)
395
+ super() {|hash,key| raise Exception.new("key #{key.inspect} not found!") }
396
+ self.merge!(hash) unless hash.nil?
397
+ end
398
+ end
399
+
400
+ end # module Datatypes
401
+
402
+ end # module