ib_ruby_proxy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +77 -0
  3. data/.gitignore +8 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +143 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +10 -0
  9. data/Gemfile.lock +74 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +130 -0
  12. data/Rakefile +12 -0
  13. data/bin/console +14 -0
  14. data/bin/generate_ruby_classes +13 -0
  15. data/bin/ibproxy +32 -0
  16. data/bin/setup +8 -0
  17. data/docs/diagrams.key +0 -0
  18. data/docs/images/architecture.png +0 -0
  19. data/examples/common.rb +14 -0
  20. data/examples/plain_req_historical_ticks.rb +19 -0
  21. data/examples/req_contract_details.rb +9 -0
  22. data/examples/req_historical_bars_data.rb +10 -0
  23. data/examples/req_historical_ticks.rb +10 -0
  24. data/examples/req_tick_by_tick_data.rb +9 -0
  25. data/ib_ruby_proxy.gemspec +43 -0
  26. data/lib/ib_ruby_proxy.rb +35 -0
  27. data/lib/ib_ruby_proxy/client/callbacks_response_handler.rb +135 -0
  28. data/lib/ib_ruby_proxy/client/client.rb +69 -0
  29. data/lib/ib_ruby_proxy/client/ib/bar.rb +36 -0
  30. data/lib/ib_ruby_proxy/client/ib/combo_leg.rb +36 -0
  31. data/lib/ib_ruby_proxy/client/ib/contract.rb +56 -0
  32. data/lib/ib_ruby_proxy/client/ib/contract_details.rb +100 -0
  33. data/lib/ib_ruby_proxy/client/ib/delta_neutral_contract.rb +26 -0
  34. data/lib/ib_ruby_proxy/client/ib/historical_tick.rb +26 -0
  35. data/lib/ib_ruby_proxy/client/ib/historical_tick_bid_ask.rb +32 -0
  36. data/lib/ib_ruby_proxy/client/ib/historical_tick_last.rb +32 -0
  37. data/lib/ib_ruby_proxy/client/ib/order.rb +262 -0
  38. data/lib/ib_ruby_proxy/client/ib/tick_attrib_bid_ask.rb +24 -0
  39. data/lib/ib_ruby_proxy/client/ib/tick_attrib_last.rb +24 -0
  40. data/lib/ib_ruby_proxy/client/ib_callbacks_observer.rb +25 -0
  41. data/lib/ib_ruby_proxy/config.yml +43 -0
  42. data/lib/ib_ruby_proxy/server/ext/array.rb +22 -0
  43. data/lib/ib_ruby_proxy/server/ext/enum.rb +20 -0
  44. data/lib/ib_ruby_proxy/server/ext/idempotent_types.rb +23 -0
  45. data/lib/ib_ruby_proxy/server/ib/bar.rb +21 -0
  46. data/lib/ib_ruby_proxy/server/ib/combo_leg.rb +21 -0
  47. data/lib/ib_ruby_proxy/server/ib/contract.rb +31 -0
  48. data/lib/ib_ruby_proxy/server/ib/contract_details.rb +53 -0
  49. data/lib/ib_ruby_proxy/server/ib/delta_neutral_contract.rb +16 -0
  50. data/lib/ib_ruby_proxy/server/ib/historical_tick.rb +16 -0
  51. data/lib/ib_ruby_proxy/server/ib/historical_tick_bid_ask.rb +19 -0
  52. data/lib/ib_ruby_proxy/server/ib/historical_tick_last.rb +19 -0
  53. data/lib/ib_ruby_proxy/server/ib/order.rb +134 -0
  54. data/lib/ib_ruby_proxy/server/ib/tick_attrib_bid_ask.rb +15 -0
  55. data/lib/ib_ruby_proxy/server/ib/tick_attrib_last.rb +15 -0
  56. data/lib/ib_ruby_proxy/server/ib_client_adapter.rb +51 -0
  57. data/lib/ib_ruby_proxy/server/ib_proxy_service.rb +85 -0
  58. data/lib/ib_ruby_proxy/server/ib_ruby_class_files_generator.rb +77 -0
  59. data/lib/ib_ruby_proxy/server/ib_ruby_class_source_generator.rb +155 -0
  60. data/lib/ib_ruby_proxy/server/ib_wrapper_adapter.rb +47 -0
  61. data/lib/ib_ruby_proxy/server/reflection/ib_class.rb +62 -0
  62. data/lib/ib_ruby_proxy/server/reflection/ib_field.rb +60 -0
  63. data/lib/ib_ruby_proxy/util/has_logger.rb +10 -0
  64. data/lib/ib_ruby_proxy/util/string_utils.rb +29 -0
  65. data/lib/ib_ruby_proxy/version.rb +3 -0
  66. data/lib/server.rb +4 -0
  67. data/sandbox/drb_performance/client.rb +46 -0
  68. data/sandbox/drb_performance/server.rb +26 -0
  69. data/vendor/TwsApi.jar +0 -0
  70. metadata +226 -0
@@ -0,0 +1,12 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ begin
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task default: :spec
10
+ rescue LoadError
11
+ # no rspec available
12
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ib_ruby_proxy"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env jruby
2
+
3
+ $LOAD_PATH.unshift './lib'
4
+ $LOAD_PATH.unshift File.join(__dir__, '..')
5
+ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
6
+
7
+ require 'ib_ruby_proxy'
8
+ require 'server'
9
+
10
+ client_code_dir = File.join(__dir__, '..', 'lib', 'ib_ruby_proxy', 'client', 'ib')
11
+ server_code_dir = File.join(__dir__, '..', 'lib', 'ib_ruby_proxy', 'server', 'ib')
12
+ IbRubyProxy::Server::IbRubyClassFilesGenerator.new(client_code_dir: client_code_dir, server_code_dir: server_code_dir).generate_all
13
+
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+
6
+ $LOAD_PATH.unshift './lib'
7
+ $LOAD_PATH.unshift File.join(__dir__, '..')
8
+ $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
9
+
10
+ require 'ib_ruby_proxy'
11
+ require 'server'
12
+
13
+ program :name, 'ibproxy'
14
+ program :version, IbRubyProxy::VERSION
15
+ program :description, 'Invoke Interactive Brokers API from Ruby'
16
+ default_command :server
17
+
18
+ command :server do |c|
19
+ c.syntax = 'ibproxy server [options]'
20
+ c.summary = 'Start ibproxy server'
21
+ c.option '--ib-port PORT', Integer, "Interactive brokers client port. #{IbRubyProxy::Server::IbProxyService::DEFAULT_IB_GATEWAY_PORT} by default (Gateway). Default for TWS is whatever"
22
+ c.option '--drb-port PORT', Integer, "Port for the served drb endpoint. #{IbRubyProxy::Server::IbProxyService::DEFAULT_DRB_PORT} by default"
23
+ c.action do |args, options|
24
+ options.default ib_port: IbRubyProxy::Server::IbProxyService::DEFAULT_IB_GATEWAY_PORT,
25
+ drb_port: IbRubyProxy::Server::IbProxyService::DEFAULT_DRB_PORT
26
+ ib_port = options.ib_port
27
+ drb_port = options.drb_port
28
+ puts "Starting with ib port #{ib_port} and drb port #{drb_port}..."
29
+ IbRubyProxy::Server::IbProxyService.new(ib_port: ib_port, drb_port: drb_port).start
30
+ end
31
+ end
32
+
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
Binary file
@@ -0,0 +1,14 @@
1
+ require 'drb'
2
+ require 'ib_ruby_proxy'
3
+
4
+ class Securities
5
+ class << self
6
+ def emini(expiration_month = '201909')
7
+ IbRubyProxy::Client::Ib::Contract.new symbol: 'ES',
8
+ sec_type: 'FUT',
9
+ currency: 'USD',
10
+ exchange: 'GLOBEX',
11
+ last_trade_date_or_contract_month: expiration_month
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ require_relative './common'
2
+
3
+ client = IbRubyProxy::Client::Client.from_drb
4
+
5
+ class CallbacksObserver < IbRubyProxy::Client::IbCallbacksObserver
6
+ def historical_ticks(_request_id, ticks, _done)
7
+ ap ticks
8
+ end
9
+ end
10
+
11
+ aapl = IbRubyProxy::Client::Ib::Contract.new symbol: 'AAPL',
12
+ sec_type: 'STK',
13
+ exchange: 'ISLAND'
14
+
15
+ client.add_ib_callbacks_observer CallbacksObserver.new
16
+ client.req_historical_ticks(18009, aapl, '20190304 12:00:00', nil, 100,
17
+ 'MIDPOINT', 1, false, nil)
18
+
19
+ sleep
@@ -0,0 +1,9 @@
1
+ require_relative './common'
2
+
3
+ client = IbRubyProxy::Client::Client.from_drb
4
+
5
+ client.req_contract_details(18009, Securities.emini) do |_callback, _request_id, contract_details|
6
+ ap contract_details
7
+ end
8
+
9
+ sleep
@@ -0,0 +1,10 @@
1
+ require_relative './common'
2
+
3
+ client = IbRubyProxy::Client::Client.from_drb
4
+
5
+ client.req_historical_data(18009, Securities.emini, '20190304 17:00:01', '1 M', '1 day',
6
+ 'TRADES', 1, 1, false, nil) do |_callback, _request_id, bar|
7
+ ap bar
8
+ end
9
+
10
+ sleep
@@ -0,0 +1,10 @@
1
+ require_relative './common'
2
+
3
+ client = IbRubyProxy::Client::Client.from_drb
4
+
5
+ client.req_historical_ticks(18009, Securities.emini, nil, '20190304 17:00:00', 100,
6
+ 'MIDPOINT', 1, false, nil) do |_callback, _request_id, ticks, _done|
7
+ ap ticks
8
+ end
9
+
10
+ sleep
@@ -0,0 +1,9 @@
1
+ require_relative './common'
2
+
3
+ client = IbRubyProxy::Client::Client.from_drb
4
+
5
+ client.req_tick_by_tick_data(18002, Securities.emini, 'Last', 0, false) do |*arguments|
6
+ ap arguments
7
+ end
8
+
9
+ sleep
@@ -0,0 +1,43 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'ib_ruby_proxy/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'ib_ruby_proxy'
7
+ spec.version = IbRubyProxy::VERSION
8
+ spec.authors = ['Jorge Manrubia']
9
+ spec.email = ['jorge.manrubia@gmail.com']
10
+
11
+ spec.summary = 'Invoke IB Java API from Ruby'
12
+ spec.description = 'Invoke IB Java API from Ruby'
13
+ spec.homepage = 'https://github.com/jorgemanrubia/ib_ruby_proxy'
14
+ spec.license = 'MIT'
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/jorgemanrubia/ib_ruby_proxy'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
23
+ 'public gem pushes.'
24
+ end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+ spec.bindir = 'exe'
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_dependency 'awesome_print'
36
+ spec.add_dependency 'commander'
37
+ spec.add_dependency 'concurrent-ruby'
38
+ spec.add_development_dependency 'bundler', '~> 2.0.1'
39
+ spec.add_development_dependency 'impersonator'
40
+ spec.add_development_dependency 'rake', '~> 10.0'
41
+ spec.add_development_dependency 'rspec', '~> 3.8.0'
42
+ spec.add_development_dependency 'rspec_junit_formatter'
43
+ end
@@ -0,0 +1,35 @@
1
+ require 'ib_ruby_proxy/version'
2
+ require 'awesome_print'
3
+ require 'drb'
4
+ require 'logger'
5
+ require 'concurrent-ruby'
6
+ require 'yaml'
7
+
8
+ Dir["#{__dir__}/ib_ruby_proxy/util/**/*.rb"].each { |file| require file }
9
+ Dir["#{__dir__}/ib_ruby_proxy/client/**/*.rb"].each { |file| require file }
10
+
11
+ module IbRubyProxy
12
+ LOGGER_LEVEL = Logger::DEBUG
13
+
14
+ # Gem logger
15
+ #
16
+ # @return [Logger]
17
+ def self.logger
18
+ @logger ||= Logger.new(STDOUT).tap do |logger|
19
+ logger.level = LOGGER_LEVEL
20
+ end
21
+ end
22
+
23
+ # Configuration options parsed from +ib_ruby_proxy/config.yml+
24
+ #
25
+ # @return [Hash]
26
+ def self.config
27
+ @config ||= begin
28
+ file_path = File.join(__dir__, 'ib_ruby_proxy/config.yml')
29
+ YAML.load_file(file_path)
30
+ end
31
+ end
32
+
33
+ class Error < StandardError
34
+ end
35
+ end
@@ -0,0 +1,135 @@
1
+ module IbRubyProxy
2
+ module Client
3
+ # This class maps callbacks with method invocations that originated them so that
4
+ # a block can be passed to the api method and it will be invoked when a callback is
5
+ # received
6
+ class CallbacksResponseHandler
7
+ IB_CALLBACKS_MAPPING = {
8
+ req_historical_ticks: { callbacks: %i[historical_ticks historical_ticks_bid_ask
9
+ historical_ticks_last],
10
+ discriminate_by_argument_nth: 0 },
11
+ req_contract_details: { callbacks: %i[contract_details contract_details_end],
12
+ discriminate_by_argument_nth: 0 },
13
+ req_tick_by_tick_data: { callbacks: %i[tick_by_tick_bid_ask tick_by_tick_all_last
14
+ tick_by_tick_mid_point],
15
+ discriminate_by_argument_nth: 0 },
16
+ req_historical_data: { callbacks: %i[historical_data historical_data_end
17
+ historical_data_update],
18
+ discriminate_by_argument_nth: 0 }
19
+ }.freeze
20
+
21
+ def initialize
22
+ @method_handlers = {}
23
+ @callback_handlers = {}
24
+ end
25
+
26
+ # Handle invoked method
27
+ #
28
+ # @param [String, Symbol] method_name
29
+ # @param [Array<Object>] arguments
30
+ # @param [Proc] block
31
+ def method_invoked(method_name, *arguments, &block)
32
+ method_name = method_name.to_sym
33
+ method_handlers[method_name]&.method_invoked(*arguments, &block)
34
+ end
35
+
36
+ # Handle callback received: it will invoke the block passed in the corresponding
37
+ # {#method_invoked}, if any.
38
+ #
39
+ # @param [Symbol] callback_name
40
+ # @param [Array<Object>] arguments
41
+ def callback_received(callback_name, *arguments)
42
+ callback_name = callback_name.to_sym
43
+ callback_handlers[callback_name]&.callback_received(callback_name, *arguments)
44
+ end
45
+
46
+ # @private
47
+ def self.for_ib
48
+ new.tap do |handler|
49
+ IbRubyProxy.config['mapped_callbacks'].each do |method, callback_config|
50
+ nth_argument = callback_config['discriminate_by_argument_nth']
51
+ handler.configure_block_callback method: method.to_sym,
52
+ callbacks: callback_config['callbacks'],
53
+ discriminate_by_argument_nth: nth_argument
54
+ end
55
+ end
56
+ end
57
+
58
+ # Configures a mapping between a method invocation and a received callback
59
+ #
60
+ # @param [String, Symbol] method
61
+ # @param [String, Symbol] callbacks
62
+ # @param [Integer, nil] discriminate_by_argument_nth The position of the argument that
63
+ # will be used to discriminate received callbacks and match them with invocation methods.
64
+ # +nil+ indicates no argument should be used for discriminating (default)
65
+ def configure_block_callback(method:, callbacks:, discriminate_by_argument_nth: nil)
66
+ validate_can_add_callback_on_method!(method)
67
+
68
+ handler = BlockCallbackHandler.new(discriminate_by_argument_nth)
69
+
70
+ configure_callbacks_handler(callbacks, handler)
71
+ configure_method_handler(method, handler)
72
+ end
73
+
74
+ private
75
+
76
+ attr_reader :method_handlers, :callback_handlers
77
+
78
+ def configure_method_handler(method, handler)
79
+ method_handlers[method.to_sym] = handler
80
+ end
81
+
82
+ def configure_callbacks_handler(callbacks, handler)
83
+ callbacks << :error
84
+ callbacks.each do |callback_name|
85
+ callback_handlers[callback_name.to_sym] = handler
86
+ end
87
+ end
88
+
89
+ def validate_can_add_callback_on_method!(method)
90
+ raise "Already configured handler for #{method}" if method_handlers[method]
91
+ end
92
+
93
+ # b_proxy_service.rb:13@private
94
+ class BlockCallbackHandler
95
+ include IbRubyProxy::Util::HasLogger
96
+
97
+ attr_reader :discriminate_by_argument_nth, :block
98
+
99
+ def initialize(discriminate_by_argument_nth)
100
+ @discriminate_by_argument_nth = discriminate_by_argument_nth
101
+ @blocks_by_discriminator = {}
102
+ end
103
+
104
+ def method_invoked(*arguments, &block)
105
+ if @discriminate_by_argument_nth
106
+ discrminator = arguments[@discriminate_by_argument_nth]
107
+ unless discrminator
108
+ raise "No argument #{@discriminate_by_argument_nth} to discriminate with?"\
109
+ " #{arguments.inspect}"
110
+ end
111
+
112
+ @blocks_by_discriminator[discrminator] = block
113
+ else
114
+ @block = block
115
+ end
116
+ nil
117
+ end
118
+
119
+ def callback_received(callback_name, *arguments)
120
+ if callback_name.to_s == 'error'
121
+ raise StandardError, arguments.join('. ')
122
+ else
123
+ block = if @discriminate_by_argument_nth
124
+ @blocks_by_discriminator[arguments[@discriminate_by_argument_nth]]
125
+ else
126
+ @block
127
+ end
128
+
129
+ block&.call(callback_name, *arguments)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,69 @@
1
+ require 'drb/timeridconv'
2
+
3
+ module IbRubyProxy
4
+ module Client
5
+ # A client for interacting with the DRb service
6
+ #
7
+ # If can be instantiated directly from the DRb service with {.create_drb_ib_client}
8
+ #
9
+ # It offers the same interface than a {IbRubyProxy::Server::IbClientAdapter} and, also,
10
+ # configures a {CallbacksResponseHandler} so that you can use Ruby blocks to capture
11
+ # callbacks invocations based on the semantics of the invoked methods.
12
+ #
13
+ # @see CallbacksResponseHandler
14
+ class Client
15
+ attr_reader :ib_client
16
+
17
+ # @return [Client]
18
+ # @param [String] host DRb host +localhost+ by default
19
+ # @param [Object] port DRb port +1992+ by default
20
+ def self.from_drb(host: 'localhost', port: 1992)
21
+ new(create_drb_ib_client(host: host, port: port))
22
+ end
23
+
24
+ # @param [IbRubyProxy::Server::IbClientAdapter] ib_client
25
+ def initialize(ib_client)
26
+ @ib_client = ib_client
27
+ @promises_by_request_id = {}
28
+ @callbacks_response_handler = CallbacksResponseHandler.for_ib
29
+ @ib_client.add_ib_callbacks_observer ResponseHandleObserver.new(@callbacks_response_handler)
30
+ end
31
+
32
+ # @private
33
+ def self.create_drb_ib_client(host:, port:)
34
+ drb_ib_client = DRbObject.new(nil, "druby://#{host}:#{port}")
35
+ DRb::DRbServer.verbose = true
36
+ DRb.install_id_conv ::DRb::TimerIdConv.new 60
37
+ DRb.start_service
38
+ drb_ib_client
39
+ end
40
+
41
+ private
42
+
43
+ def respond_to_missing?(name, include_private = false)
44
+ @ib_client.respond_to?(name, include_private)
45
+ end
46
+
47
+ def method_missing(method, *arguments, &block)
48
+ @ib_client.public_send(method, *arguments, &block)
49
+ @callbacks_response_handler.method_invoked method, *arguments, &block
50
+ end
51
+ end
52
+
53
+ # An observer that will delegate callbacks to a {CallbacksResponseHandler}
54
+ #
55
+ # @private
56
+ class ResponseHandleObserver
57
+ include DRb::DRbUndumped
58
+
59
+ def initialize(callbacks_response_handler)
60
+ @callbacks_response_handler = callbacks_response_handler
61
+ end
62
+
63
+ def update(*params)
64
+ method, *arguments = params
65
+ @callbacks_response_handler.callback_received(method, *arguments)
66
+ end
67
+ end
68
+ end
69
+ end