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,15 @@
1
+ # ---------------------------------------------
2
+ # File generated automatically by ib_ruby_proxy
3
+ # ---------------------------------------------
4
+
5
+ java_import "com.ib.client.TickAttribBidAsk"
6
+
7
+ class Java::ComIbClient::TickAttribBidAsk
8
+ def to_ruby
9
+ ruby_object = IbRubyProxy::Client::Ib::TickAttribBidAsk.new
10
+ ruby_object.bid_past_low = bidPastLow().to_ruby
11
+ ruby_object.ask_past_high = askPastHigh().to_ruby
12
+
13
+ ruby_object
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # ---------------------------------------------
2
+ # File generated automatically by ib_ruby_proxy
3
+ # ---------------------------------------------
4
+
5
+ java_import "com.ib.client.TickAttribLast"
6
+
7
+ class Java::ComIbClient::TickAttribLast
8
+ def to_ruby
9
+ ruby_object = IbRubyProxy::Client::Ib::TickAttribLast.new
10
+ ruby_object.past_limit = pastLimit().to_ruby
11
+ ruby_object.unreported = unreported().to_ruby
12
+
13
+ ruby_object
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ java_import 'com.ib.client.EClient'
2
+
3
+ module IbRubyProxy
4
+ module Server
5
+ # This is the Ruby representation of the IB Java +EClient+ class. It adapts ruby invocations
6
+ # and arguments to invoke the corresponding Java API methods.
7
+ #
8
+ # It is the object DRb clients get when connecting to the {IbProxyService proxy service}.
9
+ #
10
+ # Mimicking how the Java API works, it collaborates with an {IbWrapperAdapter} that
11
+ # corresponds with an +EWrapper+ in the Java world. It works by observing callbacks
12
+ # in this wrapper.
13
+ #
14
+ # The usage of observers as a communication mechanism is because it is supported
15
+ # by DRb and, being DRb a standard implemented in JRuby, enables communicating Ruby and JRuby.
16
+ # If supporting other rubies was not required this level of indirection would be unnecessary.
17
+ class IbClientAdapter
18
+ extend IbRubyProxy::Util::StringUtils
19
+
20
+ attr_reader :ib_client, :ib_wrapper_adapter
21
+
22
+ # @param [com.ib.client.EClient] ib_client IB +EClient+ object
23
+ # @param [IbWrapperAdapter] ib_wrapper_adapter
24
+ def initialize(ib_client, ib_wrapper_adapter)
25
+ @ib_client = ib_client
26
+ @ib_wrapper_adapter = ib_wrapper_adapter
27
+ end
28
+
29
+ # @param [IbRubyProxy::Client::IbCallbacksObserver] ib_callbacks_observer
30
+ def add_ib_callbacks_observer(ib_callbacks_observer)
31
+ ib_wrapper_adapter.add_observer(ib_callbacks_observer)
32
+ end
33
+
34
+ # @private
35
+ def self.define_ruby_method_for(java_method)
36
+ ruby_method_name = to_underscore(java_method.name)
37
+
38
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
+ def #{ruby_method_name}(*arguments)
40
+ ib_arguments = arguments.collect(&:to_ib)
41
+ @ib_client.#{java_method.name} *ib_arguments
42
+ end
43
+ RUBY
44
+ end
45
+
46
+ EClient.java_class.declared_instance_methods.each do |java_method|
47
+ define_ruby_method_for(java_method)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,85 @@
1
+ require 'ib_ruby_proxy/version'
2
+ require_relative 'ib_wrapper_adapter'
3
+ require_relative 'ib_client_adapter'
4
+ require 'drb'
5
+
6
+ java_import 'com.ib.client.EClientSocket'
7
+ java_import 'com.ib.client.EJavaSignal'
8
+ java_import 'com.ib.client.EClientSocket'
9
+ java_import 'com.ib.client.EReader'
10
+
11
+ module IbRubyProxy
12
+ module Server
13
+ # Proxy service for invoking IB api through
14
+ # {https://ruby-doc.org/stdlib-2.6.1/libdoc/drb/rdoc/DRb.html DRb}
15
+ #
16
+ # The proxy does essentially 2 things:
17
+ #
18
+ # * Starts a DRb process you can connect to invoke the IB api. This will expose an {IbClientAdapter}
19
+ # object to its clients
20
+ # * Starts an IB message-processing thread that will dispatch messages sent to IB client app (
21
+ # gateway or TWS)
22
+ class IbProxyService
23
+ DEFAULT_IB_GATEWAY_PORT = 4002
24
+ DEFAULT_DRB_PORT = 1992
25
+
26
+ # @param [String] ib_host Hostname for the IB client app (gateway or TWS). Default +localhost+
27
+ # @param [Integer] ib_port Port for hte IB client app (gateway or TWS). Default +4002+ (gateway)
28
+ # @param [String] drb_host Hostname for the DRB process. Default +localhost+
29
+ # @param [Integer] drb_port Port for the . Default +1992+
30
+ def initialize(ib_host: 'localhost', ib_port: DEFAULT_IB_GATEWAY_PORT,
31
+ drb_host: 'localhost', drb_port: DEFAULT_DRB_PORT)
32
+ @ib_host = ib_host
33
+ @ib_port = ib_port
34
+ @drb_host = drb_host
35
+ @drb_port = drb_port
36
+
37
+ @wrapper = IbRubyProxy::Server::IbWrapperAdapter.new
38
+ @client = wrapper.client
39
+ @signal = wrapper.signal
40
+
41
+ connect
42
+ end
43
+
44
+ # Connects to IB and starts the DRb process
45
+ #
46
+ # Clients of the DRb service will get an {IbClientAdapter} object to interact with the IB api
47
+ # @return [void]
48
+ def start
49
+ DRb.start_service("druby://#{drb_host}:#{drb_port}", ib_client_adapter, verbose: true)
50
+ start_ib_message_processing_thread
51
+ puts "Ib proxy server started at druby://#{drb_host}:#{drb_port}. Connected to IB at"\
52
+ " #{ib_host}:#{ib_port}"
53
+ DRb.thread.join
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :wrapper, :client, :signal, :ib_host, :ib_port, :drb_host, :drb_port
59
+
60
+ def start_ib_message_processing_thread
61
+ reader = EReader.new(client, signal)
62
+ reader.start
63
+
64
+ Thread.new do
65
+ while client.isConnected
66
+ signal.waitForSignal
67
+ begin
68
+ reader.processMsgs
69
+ rescue StandardError => e
70
+ puts "Error while processing ib message: #{e}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def connect
77
+ client.eConnect(ib_host, ib_port, 2)
78
+ end
79
+
80
+ def ib_client_adapter
81
+ @ib_client_adapter ||= IbRubyProxy::Server::IbClientAdapter.new(client, wrapper)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,77 @@
1
+ module IbRubyProxy
2
+ module Server
3
+ # Source code generator that interacts with {IbRubyClassSourceGenerator} and writes down the
4
+ # actual files with the source for both Ruby classes representing IB classes and Ruby extensions
5
+ # for these.
6
+ class IbRubyClassFilesGenerator
7
+ attr_reader :client_code_dir, :server_code_dir
8
+
9
+ # @param [String] client_code_dir
10
+ # @param [String] server_code_dir
11
+ def initialize(client_code_dir:, server_code_dir:)
12
+ @client_code_dir = client_code_dir
13
+ @server_code_dir = server_code_dir
14
+
15
+ import_ruby_classes
16
+ end
17
+
18
+ # Generate client files and server class extensions
19
+ #
20
+ # It will format the generated code with {https://github.com/ruby-formatter/rufo Rufo}.
21
+ #
22
+ # @see IbRubyClassSourceGenerator#ruby_class_source
23
+ # @see IbRubyClassSourceGenerator#ib_class_extension_source
24
+ def generate_all
25
+ do_generate_all
26
+ format_code
27
+ end
28
+
29
+ private
30
+
31
+ def import_ruby_classes
32
+ ruby_classes.each do |class_name|
33
+ java_import "com.ib.client.#{class_name}"
34
+ end
35
+ end
36
+
37
+ def ruby_classes
38
+ @ruby_classes ||= IbRubyProxy.config['classes']
39
+ end
40
+
41
+ def do_generate_all
42
+ ruby_classes.each do |class_name|
43
+ generate_files class_name
44
+ end
45
+ end
46
+
47
+ def generate_files(class_name)
48
+ ib_class = Java::ComIbClient.const_get(class_name)
49
+ generator = IbRubyClassSourceGenerator.new(ib_class,
50
+ namespace: 'IbRubyProxy::Client::Ib')
51
+
52
+ file_name = "#{IbRubyProxy::Util::StringUtils.to_underscore(class_name)}.rb"
53
+ generate_client_file(generator, file_name)
54
+ generate_server_file(generator, file_name)
55
+ end
56
+
57
+ def generate_client_file(generator, file_name)
58
+ target_file = File.join(client_code_dir, file_name)
59
+ puts "Generating client file: #{target_file}..."
60
+ File.open(target_file, 'w') { |file| file.write(generator.ruby_class_source) }
61
+ end
62
+
63
+ def generate_server_file(generator, file_name)
64
+ target_file = File.join(server_code_dir, file_name)
65
+ puts "Generating server file: #{target_file}..."
66
+ File.open(target_file, 'w') { |file| file.write(generator.ib_class_extension_source) }
67
+ end
68
+
69
+ def format_code
70
+ [server_code_dir, client_code_dir].each do |dir|
71
+ puts "Formatting #{dir}..."
72
+ system "rufo #{dir}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,155 @@
1
+ module IbRubyProxy
2
+ module Server
3
+ # Given a value object class from Interactive Broker API, this generator can build Ruby source
4
+ # code for:
5
+ #
6
+ # * A ruby class to manipulate the same class in Ruby
7
+ # * An extension to the original Java class for converting instances into Ruby
8
+ class IbRubyClassSourceGenerator
9
+ include IbRubyProxy::Util::StringUtils
10
+
11
+ attr_reader :ib_class, :namespace_list
12
+
13
+ # @param [Class] ib_class Class from IB broker to mimic in Ruby. It is meant to be used with
14
+ # value objects, as only properties are translated. That is the case for classes that
15
+ # represent either arguments or returned values (via callbacks) in the API.
16
+ # @param [String] namespace The namespace to generate the classes in. Default
17
+ # +IbRubyProxy::Client::Ib+
18
+ def initialize(ib_class, namespace: 'IbRubyProxy::Client::Ib')
19
+ @ib_class = Reflection::IbClass.new(ib_class)
20
+ @namespace_list = namespace.split('::')
21
+ end
22
+
23
+ # Source for a ruby class translates into Ruby the Java properties of the target
24
+ # ib class.
25
+ #
26
+ # It generates a {Struct} with the list of properties, that admits a keyword-based
27
+ # constructor, and that includes a +to_ib+ method for converting the ruby back into its
28
+ # original Java counterpart.
29
+ #
30
+ # @return [String]
31
+ def ruby_class_source
32
+ <<-RUBY
33
+ #{header}
34
+
35
+ #{generate_ruby_class}
36
+ RUBY
37
+ end
38
+
39
+ # Source for a ruby class that extends the original IB Java class with a +to_ruby+ method,
40
+ # to convert Java Ib objects into their Ruby counterparts generated in {#ruby_class_source}
41
+ #
42
+ # @return [String]
43
+ def ib_class_extension_source
44
+ <<-RUBY
45
+ #{header}
46
+
47
+ #{generate_ib_class_extension}
48
+ RUBY
49
+ end
50
+
51
+ private
52
+
53
+ def header
54
+ <<-RUBY
55
+ # ---------------------------------------------
56
+ # File generated automatically by ib_ruby_proxy
57
+ # ---------------------------------------------
58
+ RUBY
59
+ end
60
+
61
+ def generate_ib_class_extension
62
+ <<-RUBY
63
+ java_import "com.ib.client.#{ib_class.name}"
64
+
65
+ class #{ib_class.full_name}
66
+ #{generate_to_ruby_method}
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ def generate_ruby_class
72
+ <<-RUBY
73
+ #{generate_namespace_open}
74
+
75
+ #{generate_class_declaration}
76
+ #{generate_constructor}
77
+ #{generate_to_ib_method}
78
+ end
79
+
80
+ #{generate_namespace_close}
81
+ RUBY
82
+ end
83
+
84
+ def generate_namespace_open
85
+ namespace_list.collect { |namespace| "module #{namespace}" }.join("\n")
86
+ end
87
+
88
+ def generate_namespace_close
89
+ namespace_list.length.times.collect { 'end' }.join("\n")
90
+ end
91
+
92
+ def generate_class_declaration
93
+ struct_init_properties = ib_class.ruby_property_names.collect { |property| ":#{property}" }
94
+ .join(', ')
95
+ struct_init_properties << ', keyword_init: true'
96
+
97
+ <<-RUBY
98
+ #{ib_class.name} = Struct.new(#{struct_init_properties}) do
99
+ RUBY
100
+ end
101
+
102
+ def generate_constructor
103
+ constructor_declarations = ib_class.zipped_ruby_and_java_properties
104
+ .collect do |ruby_property, java_field|
105
+ "#{ruby_property}: #{java_field.default_value_as_string}"
106
+ end
107
+
108
+ assignment_statements = ib_class.ruby_property_names.collect do |ruby_property|
109
+ <<-RUBY
110
+ self.#{ruby_property} = #{ruby_property}
111
+ RUBY
112
+ end
113
+ <<-RUBY
114
+ def initialize(#{constructor_declarations.join(', ')})
115
+ #{assignment_statements.join('')}
116
+ end
117
+ RUBY
118
+ end
119
+
120
+ def generate_to_ib_method
121
+ property_copy_sentences = ib_class.zipped_ruby_and_java_properties
122
+ .collect do |ruby_property, java_field|
123
+ <<-RUBY
124
+ ib_object.#{java_field.name}(#{ruby_property}).to_java
125
+ RUBY
126
+ end
127
+
128
+ <<-RUBY
129
+ def to_ib
130
+ ib_object = #{ib_class.klass.name}.new
131
+ #{property_copy_sentences.join('')}
132
+ ib_object
133
+ end
134
+ RUBY
135
+ end
136
+
137
+ def generate_to_ruby_method
138
+ property_copy_sentences = ib_class.zipped_ruby_and_java_properties
139
+ .collect do |ruby_property, java_field|
140
+ <<-RUBY
141
+ ruby_object.#{ruby_property} = #{java_field.name}().to_ruby
142
+ RUBY
143
+ end
144
+
145
+ <<-RUBY
146
+ def to_ruby
147
+ ruby_object = #{namespace_list.join('::')}::#{ib_class.name}.new
148
+ #{property_copy_sentences.join('')}
149
+ ruby_object
150
+ end
151
+ RUBY
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,47 @@
1
+ java_import 'com.ib.client.EWrapper'
2
+ require 'drb/observer'
3
+
4
+ module IbRubyProxy
5
+ module Server
6
+ # Ruby representation of the IB +EWrapper+ class. It delegates received callbacks by
7
+ # triggering an observer notification. It will translate the name of the java method to
8
+ # Ruby (underscore), as well as translate the parameters from IB to the Ruby world.
9
+ #
10
+ # @see IbClientAdapter
11
+ class IbWrapperAdapter
12
+ # include DRb::DRbObservable
13
+ include EWrapper
14
+ include DRb::DRbObservable
15
+ include IbRubyProxy::Util::HasLogger
16
+ extend IbRubyProxy::Util::StringUtils
17
+
18
+ attr_reader :signal, :client
19
+
20
+ def initialize
21
+ @signal = EJavaSignal.new
22
+ @client = EClientSocket.new(self, @signal)
23
+ end
24
+
25
+ # @private
26
+ def self.define_ruby_method_for(java_method)
27
+ ruby_method_name = to_underscore(java_method.name)
28
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
29
+ def #{java_method.name}(*arguments)
30
+ ruby_arguments = arguments.collect(&:to_ruby)
31
+ if count_observers > 0
32
+ changed
33
+ notify_observers *(["#{ruby_method_name}"] + ruby_arguments)
34
+ else
35
+ logger.debug "Received #{ruby_method_name}"
36
+ logger.debug ruby_arguments.inspect
37
+ end
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ EWrapper.java_class.declared_instance_methods.each do |java_method|
43
+ define_ruby_method_for(java_method)
44
+ end
45
+ end
46
+ end
47
+ end