ib_ruby_proxy 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 (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