diff --git a/.gitignore b/.gitignore index 5e1422c..603cdd1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.gem *.rbc +*.swp +*.swo /.config /coverage/ /InstalledFiles diff --git a/Gemfile b/Gemfile index 3344429..345fbcc 100644 --- a/Gemfile +++ b/Gemfile @@ -11,6 +11,7 @@ gemspec gem 'rspec' gem 'rspec-its' +gem 'rspec-given' gem 'rspec-collection_matchers' gem 'guard' gem 'guard-rspec' diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index bec9bc3..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,104 +0,0 @@ -GIT - remote: https://github.com/ohler55/ox.git - revision: 67ce6ecb45a0d1354e1f8ed9a155826ba986e21e - specs: - ox (2.13.4) - -PATH - remote: . - specs: - ib-api (972.2) - activemodel - activesupport (>= 6.0) - -GEM - remote: https://rubygems.org/ - specs: - activemodel (6.1.2.1) - activesupport (= 6.1.2.1) - activesupport (6.1.2.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - zeitwerk (~> 2.3) - coderay (1.1.3) - concurrent-ruby (1.1.8) - diff-lcs (1.4.4) - ffi (1.13.1) - formatador (0.2.5) - guard (2.16.2) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.9.12) - shellany (~> 0.0) - thor (>= 0.18.1) - guard-compat (1.2.1) - guard-rspec (4.7.3) - guard (~> 2.1) - guard-compat (~> 1.1) - rspec (>= 2.99.0, < 4.0) - i18n (1.8.9) - concurrent-ruby (~> 1.0) - listen (3.2.1) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - lumberjack (1.2.8) - method_source (1.0.0) - minitest (5.14.3) - nenv (0.3.0) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) - pry (0.13.1) - coderay (~> 1.1) - method_source (~> 1.0) - rake (13.0.1) - rb-fsevent (0.10.4) - rb-inotify (0.10.1) - ffi (~> 1.0) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-collection_matchers (1.2.0) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.9.3) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-its (1.3.0) - rspec-core (>= 3.0.0) - rspec-expectations (>= 3.0.0) - rspec-mocks (3.9.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.3) - shellany (0.0.1) - thor (1.0.1) - tzinfo (2.0.4) - concurrent-ruby (~> 1.0) - value_semantics (3.6.0) - zeitwerk (2.4.2) - -PLATFORMS - ruby - -DEPENDENCIES - bundler - guard - guard-rspec - ib-api! - ox! - rake (~> 13.0) - rspec - rspec-collection_matchers - rspec-its - value_semantics - -BUNDLED WITH - 1.17.3 diff --git a/README.md b/README.md index d031e93..86a8053 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ # ib-api Ruby interface to Interactive Brokers' TWS API -Reimplementation of the basic functions of ib-ruby +Reimplementation of ib-ruby --- -__STATUS: Preparing for a new GEM-Release, scheduled for July__ (delayed to August) +__STATUS: Gem-Release is still pending + --- __Documentation: [https://ib-ruby.github.io/ib-doc/](https://ib-ruby.github.io/ib-doc/)__ (_work in progress_) ---- -`ib-ruby` offers a modular access to the TWS-API-Interface of Interactive Brokers. - -`ib-api` provides a simple interface to low-level TWS API-calls. +`ib-api` offers a modular access to the TWS-API-Interface of Interactive Brokers. ---- @@ -23,7 +22,7 @@ Install in the usual way $ gem install ib-api ``` -In its plain vanilla usage, it just exchanges messages with the TWS. Any response is stored in the `recieved-Array`. +In its plain vanilla usage, it just exchanges messages with the TWS. Any response is stored in the `received-array`. It needs just a few lines of code to place an order @@ -52,53 +51,43 @@ puts ib.recieved[:OrderStatus].to_human ``` -##### User-specific Actions -Besides storing any TWS-response in an array, callbacks are implemented. +## Plugins + +**IB-API** ships with simple plugins to facilitate automations -The user subscribes to a certain response and defines the actions in a typically ruby manner. These actions -can be defined globaly ```ruby -ib = IB::Connection.new do |tws| - # Subscribe to TWS alerts/errors and order-related messages - tws.subscribe(:Alert, :OpenOrder, :OrderStatus, :OpenOrderEnd) { |msg| puts msg.to_human } - end +require 'ib-api' +# connect with default parameters +ib = IB::Connection.new do | c | + c.activate_plugin "verify" +end +g = IB::Stock.new symbol: 'GE' +puts g.verify.first.attributes +{:symbol=>"GE", :sec_type=>"STK", :last_trading_day=>"", :strike=>0.0, :right=>"", :exchange=>"SMART", :currency=>"USD", :local_symbol=>"GE", :trading_class=>"GE", :con_id=>498843743, :multiplier=>0, :primary_exchange=>"NYSE", } ``` -or occationally +Currently implemented plugins + +* connection-tools: ensure that a connection is established and active +* verify: get contract details from the tws +* symbols: use predefined symbols +* managed-accounts: fetch and organize account- and portfoliovalues +* advanced-account: perform account-based previewing, opening, modifying and closing of Positions +* process-orders: account-based bookkeeping of orders +* auto-adjust: properly adjust the orderprice to the next valid min-tick of the contract +* market-price: fetch the current market-price of a contract +* eod: retrieve EOD-Data for the given contract +* greeks: read current option greeks +* roll: easy rolling of futures and options +* option-chain: build option-chains for given strikes and expiries +* spread-prototypes: create limit, stop, market, etc. orders through prototypes +* probability-of-expiring: calculate the probability of expiring for the option-contract + -```ruby - # first define actions - q = Queue.new # Initialize as Queue - request_id = nil # declare variable - a = ib.subscribe(:Alert, :ContractData, :ContractDataEnd ) do |msg| - case msg - when Messages::Incoming::Alert - q.close if msg.code == 200 # No security found - when Messages::Incoming::ContractData # security returned - q.push msg.contract if msg.request_id == request_id - when Messages::Incoming::ContractDataEnd - q.close if msg.request_id == request_id - end # case - end - # perform request - request_id = ib.send_message :RequestContractData, :contract => Stock.new(symbol: 'T') - - while contract = q.pop - puts contract.as_table - end -┌───────┬────────┬──────────┬──────────┬────────┬────────────┬───────────────┬───────┬────────┬──────────┐ -│ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │ -╞═══════╪════════╪══════════╪══════════╪════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡ -│ Stock │ T │ 37018770 │ SMART │ │ │ T │ │ │ USD │ -└───────┴────────┴──────────┴──────────┴────────┴────────────┴───────────────┴───────┴────────┴──────────┘ - - ib.unsubscribe a # release subscriptions - -``` ## Minimal TWS-Version -`ib-api` is tested via the _stable IB-Gateway_ (Version 9.72) and should work with any current tws-installation. +`ib-api` is tested via the _stable IB-Gateway_ (Version 10.19) and should work with any current tws-installation. ## Tests diff --git a/VERSION b/VERSION index c65566e..d35c4ab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -972.5 +10.19.1 diff --git a/api.gemspec b/api.gemspec index 1123c74..480eb65 100644 --- a/api.gemspec +++ b/api.gemspec @@ -42,4 +42,12 @@ Gem::Specification.new do |spec| spec.add_dependency 'activemodel' spec.add_dependency 'ox' spec.add_dependency 'terminal-table' + spec.add_dependency 'zeitwerk' + spec.add_dependency 'workflow', '~> 3.1' +# spec.add_dependency 'dry-schema' +# spec.add_dependency 'dry-struct' +# spec.add_dependency 'dry-core' +# spec.add_dependency 'dry-configurable' +# spec.add_dependency 'dry-monads' # future use + end diff --git a/bin/console b/bin/console index a0a2a82..ad3f8a2 100755 --- a/bin/console +++ b/bin/console @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -### loads the active-orient environment +### loads the active-orient environment ### and starts an interactive shell ### ### Parameter: t)ws | g)ateway (or number of port ) Default: Gateway , @@ -17,9 +17,9 @@ class Array # i.e # # 2.5.0 :006 > C.received[:OpenOrder].local_id - # => [16, 17, 21, 20, 19, 8, 7] + # => [16, 17, 21, 20, 19, 8, 7] # 2.5.0 :007 > C.received[:OpenOrder].contract.to_human - # => ["", "", "", "", "", "", ""] + # => ["", "", "", "", "", "", ""] # # its included only in the console, for inspection purposes @@ -31,61 +31,66 @@ class Array end end # Array +class Object + def inspect + respond_to?(:to_human) ? to_human : super + end +end -# read items from console.yml -read_yml = -> (key) do +# read items from console.yml + read_yml = -> (key) do YAML::load_file( File.expand_path('../console.yml',__FILE__))[key] end - - puts - puts ">> IB-Core Interactive Console <<" + puts + puts ">> IB-API Interactive Console <<" puts '-'* 45 - puts + puts puts "Namespace is IB ! " puts puts '-'* 45 include IB require 'irb' client_id = ARGV[1] || read_yml[:client_id] - specified_port = ARGV[0] || 'Gateway' - port = case specified_port - when Integer - specified_port # just use the number + specified_host = ARGV[0] || 'Gateway' + host = case specified_host when /^[gG]/ read_yml[:gateway] when /^[Tt]/ read_yml[:tws] + else + raise "Specify target from console.yml: `g|t` instead of #{specified_host}" end - ARGV.clear - - ## The Block takes instructions which are executed after initializing all instance-variables - ## and prior to the connection-process - ## Here we just subscribe to some events - C = Connection.new client_id: client_id, port: port do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| - c.subscribe( :ContractData, :BondContractData) { |msg| c.logger.info { msg.contract.to_human } } - c.subscribe( :Alert, :ContractDataEnd, :ManagedAccounts, :OrderStatus ) {| m| c.logger.info { m.to_human } } - c.subscribe( :PortfolioValue, :AccountValue, :OrderStatus, :OpenOrderEnd, :ExecutionData ) {| m| c.logger.info { m.to_human }} -# c.subscribe :ManagedAccounts do |msg| -# puts "------------------------------- Managed Accounts ----------------------------------" -# puts "Detected Accounts: #{msg.accounts.account.join(' -- ')} " -# puts -# end + C = Connection.new client_id: client_id, host: host + C.logger.level = Logger::WARN + + C.subscribe(:Alert){ |m| puts "A: "+ m.message } + C.subscribe(:AccountUpdateTime){ } + + C.received = true + C.activate_plugin :connection_tools, :symbols, :market_price, + "order-prototypes", "spread-prototypes", + "advanced_account", 'process_orders' + C.logger.level = Logger::INFO + puts C.workflow_state + C.get_account_data + C.request_open_orders + C.logger.level = Logger::ERROR + puts "Connection established on #{host}" - c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } - end - #C.logger.level = Logger::FATAL unless C.received[:OpenOrder].blank? - puts "------------------------------- OpenOrders ----------------------------------" - puts C.received[:OpenOrder].to_human.join "\n" + puts "---------------------------------------- OpenOrders -------------------------------------------" + puts C.clients.map{ |c| c.orders.map &:to_human }.flatten.join("\n") end - puts "Connection established on Port #{port}, client_id #{client_id} used" + puts "" + puts Terminal::Table.new title: 'Active Plugins', + rows: C.plugins.delete_if{ |x| x =~ /\// }.sort.each_slice(4), + style: { border: :unicode } puts puts "----> C points to the connection-instance" puts - puts "some basic Messages are subscribed and accordingly displayed" puts '-'* 45 IRB.start(__FILE__) diff --git a/bin/console.yml b/bin/console.yml index 392bd8b..ac9d8dd 100644 --- a/bin/console.yml +++ b/bin/console.yml @@ -1,3 +1,3 @@ -:gateway: 4002 -:tws: 7496 +:gateway: "localhost:4002" +:tws: "tws:7496" :client_id: 2000 diff --git a/bin/simple b/bin/simple new file mode 100755 index 0000000..3c8c670 --- /dev/null +++ b/bin/simple @@ -0,0 +1,91 @@ +#!/usr/bin/env ruby +### loads the active-orient environment +### and starts an interactive shell +### +### Parameter: t)ws | g)ateway (or number of port ) Default: Gateway , +### client_id , Default 2000 +### +### Define Parameter in file console.yml +### +require 'bundler/setup' +require 'yaml' + +require 'ib-api' + +class Array + # enables calling members of an array. which are hashes by it name + # i.e + # + # 2.5.0 :006 > C.received[:OpenOrder].local_id + # => [16, 17, 21, 20, 19, 8, 7] + # 2.5.0 :007 > C.received[:OpenOrder].contract.to_human + # => ["", "", "", "", "", "", ""] + # + # its included only in the console, for inspection purposes + + def method_missing(method, *key) + unless method == :to_hash || method == :to_str #|| method == :to_int + return self.map{|x| x.public_send(method, *key)} + end + + end +end # Array + + +# read items from console.yml +read_yml = -> (key) do + YAML::load_file( File.expand_path('../console.yml',__FILE__))[key] + end + + + puts + puts ">> IB-Core Interactive Console <<" + puts '-'* 45 + puts + puts "Namespace is IB ! " + puts + puts '-'* 45 + include IB + require 'irb' + client_id = ARGV[1] || read_yml[:client_id] + specified_host = ARGV[0] || 'Gateway' + host = case specified_host + when Integer + specified_port # just use the number + when /^[gG]/ + read_yml[:gateway] + when /^[Tt]/ + read_yml[:tws] + end + + ARGV.clear + + ## The Block takes instructions which are executed after initializing all instance-variables + ## and prior to the connection-process + ## Here we just subscribe to some events + C = Connection.new client_id: client_id, host: host do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| + c.received = true + c.subscribe( :ContractData, :BondContractData) { |msg| c.logger.info { msg.contract.to_human } } + c.subscribe( :Alert, :ContractDataEnd, :ManagedAccounts, :OrderStatus ) {| m| c.logger.info { m.to_human } } + c.subscribe( :PortfolioValue, :AccountValue, :OrderStatus, :OpenOrderEnd, :ExecutionData ) {| m| c.logger.info { m.to_human }} +# c.subscribe :ManagedAccounts do |msg| +# puts "------------------------------- Managed Accounts ----------------------------------" +# puts "Detected Accounts: #{msg.accounts.account.join(' -- ')} " +# puts +# end + + c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } + end + #C.logger.level = Logger::FATAL + unless C.received[:OpenOrder].blank? + puts "------------------------------- OpenOrders ----------------------------------" + puts C.received[:OpenOrder].to_human.join "\n" + end + puts "Connection established through #{host}, client_id #{client_id} used" + puts + puts "----> C points to the connection-instance" + puts + puts "some basic Messages are subscribed and accordingly displayed" + puts '-'* 45 + + IRB.start(__FILE__) diff --git a/changelog.md b/changelog.md index e30c643..30d8dbc 100644 --- a/changelog.md +++ b/changelog.md @@ -17,3 +17,16 @@ Changelog | | Preparation of a Gem-Release | | 23.2.2021 | Fixed retrieving of ContractDetail requests of Options with strikes < 1 | | Gem Release | + +| 1.4.2024 | Proper monkey patching of classes through class_extensions (Prepare for Zeitwerk, V10) +| 2.4.2024 | Renaming of IBSupport and IBSocket to IB::Support and IB::Socket (Prepare for Zeitwerk, V10) +| 4.4.2024 | Apply Zeitwerk, V10 + Put `model` to the root directory (the files are then easily fetched through zeitwerk) + Reorganizing Messages. One message class per file. Keeping general incoming and outgoing-files +| 1.1.2025 | introducing plugins +| | using a state machine to organize access to advanced featurs + +| 7.5.2025 | Disabled verify buffer size to enable receiving historical datastreams +| | Removed contract#verify! in favour of #contract.verify + + diff --git a/conditions/ib/execution_condition.rb b/conditions/ib/execution_condition.rb new file mode 100644 index 0000000..0209d9e --- /dev/null +++ b/conditions/ib/execution_condition.rb @@ -0,0 +1,31 @@ +module IB + class ExecutionCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + + def condition_type + 5 + end + + def self.make buffer + m =self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int + + the_contract = IB::Contract.new sec_type: buffer.read_string, + exchange: buffer.read_string, + symbol: buffer.read_string + m.contract = the_contract + m + end + + def serialize + super << contract[:sec_type] <<(contract.primary_exchange.presence || contract.exchange) << contract.symbol + end + + def self.fabricate contract + self.new contract: verify_contract_if_necessary( contract ) + end + + end + + +end # module diff --git a/conditions/ib/margin_condition.rb b/conditions/ib/margin_condition.rb new file mode 100644 index 0000000..9a203b0 --- /dev/null +++ b/conditions/ib/margin_condition.rb @@ -0,0 +1,28 @@ +module IB + + class MarginCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + + prop :percent + + def condition_type + 4 + end + + def self.make buffer + self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int, + percent: buffer.read_int + end + + def serialize + super << self[:operator] << percent + end + def self.fabricate operator, percent + error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator + self.new operator: operator, + percent: percent + end + end + +end # module diff --git a/conditions/ib/order_condition.rb b/conditions/ib/order_condition.rb new file mode 100644 index 0000000..f5da9c2 --- /dev/null +++ b/conditions/ib/order_condition.rb @@ -0,0 +1,29 @@ +module IB + class OrderCondition < IB::Base + include BaseProperties + using IB::Support # refine Array-method for decoding of IB-Messages + + prop :operator, # 1 -> " >= " , 0 -> " <= " see /lib/ib/constants # 338f + :conjunction_connection, # "o" -> or "a" + :contract + + def self.verify_contract_if_necessary c + c.con_id.to_i.zero? ||( c.primary_exchange.blank? && c.exchange.blank?) ? c.verify! : c + end + def condition_type + error "condition_type method is abstract" + end + def default_attributes + super.merge( operator: ">=" , conjunction_connection: :and ) + end + + def serialize_contract_by_con_id + [ contract.con_id , contract.primary_exchange.presence || contract.exchange ] + end + + def serialize + [ condition_type, self[:conjunction_connection] ] + end + end + +end # module diff --git a/conditions/ib/percent_change_condition.rb b/conditions/ib/percent_change_condition.rb new file mode 100644 index 0000000..0bb3e2e --- /dev/null +++ b/conditions/ib/percent_change_condition.rb @@ -0,0 +1,34 @@ +module IB + + class PercentChangeCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + prop :percent_change + include BaseProperties + + def condition_type + 7 + end + + def self.make buffer + m = self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int, + percent_change: buffer.read_decimal + + the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string + m.contract = the_contract + m + end + + def serialize + super << self[:operator] << percent_change << serialize_contract_by_con_id + + end + # dsl: PercentChangeCondition.fabricate some_contract, ">=", "5%" + def self.fabricate contract, operator, change + error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator + self.new operator: operator, + percent_change: change.to_i, + contract: verify_contract_if_necessary( contract ) + end + end +end # module diff --git a/conditions/ib/price_condition.rb b/conditions/ib/price_condition.rb new file mode 100644 index 0000000..4003a31 --- /dev/null +++ b/conditions/ib/price_condition.rb @@ -0,0 +1,44 @@ +module IB + + + class PriceCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + include BaseProperties + prop :price, + :trigger_method # see /models/ib/order.rb# 51 ff and /lib/ib/constants # 210 ff + + def default_attributes + super.merge( :trigger_method => :default ) + end + + def condition_type + 1 + end + + def self.make buffer + m= self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int, + price: buffer.read_decimal + + the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string + m.contract = the_contract + m.trigger_method = buffer.read_int + m + + end + + def serialize + super << self[:operator] << price << serialize_contract_by_con_id << self[:trigger_method] + end + + # dsl: PriceCondition.fabricate some_contract, ">=", 500 + def self.fabricate contract, operator, price + error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator + self.new operator: operator, + price: price.to_i, + contract: verify_contract_if_necessary( contract ) + end + + end + +end # module diff --git a/conditions/ib/time_condition.rb b/conditions/ib/time_condition.rb new file mode 100644 index 0000000..9c87881 --- /dev/null +++ b/conditions/ib/time_condition.rb @@ -0,0 +1,42 @@ +module IB + + + class TimeCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + include BaseProperties + prop :time + + def condition_type + 3 + end + + def self.make buffer + self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int, + time: buffer.read_parse_date + end + + def serialize + t = self[:time] + if t.is_a?(String) && t =~ /^\d{8}\z/ # expiry-format yyymmmdd + self.time = DateTime.new t[0..3],t[4..5],t[-2..-1] + end + serialized_time = case self[:time] # explicity formatting of time-object + when String + self[:time] + when DateTime + self[:time].gmtime.strftime("%Y%m%d %H:%M:%S %Z") + when Date, Time + self[:time].strftime("%Y%m%d %H:%M:%S") + end + + super << self[:operator] << serialized_time + end + + def self.fabricate operator, time + self.new operator: operator, + time: time + end + end + +end # module diff --git a/conditions/ib/volume_condition.rb b/conditions/ib/volume_condition.rb new file mode 100644 index 0000000..ebd28ef --- /dev/null +++ b/conditions/ib/volume_condition.rb @@ -0,0 +1,36 @@ +module IB + class VolumeCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + include BaseProperties + + prop :volume + + def condition_type + 6 + end + + def self.make buffer + m = self.new conjunction_connection: buffer.read_string, + operator: buffer.read_int, + volumne: buffer.read_int + + the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string + m.contract = the_contract + m + end + + def serialize + + super << self[:operator] << volume << serialize_contract_by.con_id + end + + # dsl: VolumeCondition.fabricate some_contract, ">=", 50000 + def self.fabricate contract, operator, volume + error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator + self.new operator: operator, + volume: volume, + contract: verify_contract_if_necessary( contract ) + end + end + +end # module diff --git a/lib/class_extensions.rb b/lib/class_extensions.rb new file mode 100644 index 0000000..151a1b4 --- /dev/null +++ b/lib/class_extensions.rb @@ -0,0 +1,167 @@ +# Include the method `to_bool` to some basic classes +# +# Prepare the output of arrays via Terminal::Table +# +# Define the method `count_duplicates` for Arrays +# +require 'date' + +module ClassExtensions + module Array + module DuplicatesCounter + def count_duplicates + self.each_with_object(Hash.new(0)) { |element, counter| counter[element] += 1 }.sort_by{|k,v| -v}.to_h + end + end + + module TablePresenter + def as_table(&b) + the_table_header = first.table_header(&b) + the_table_rows = map &:table_row + Terminal::Table.new headings: the_table_header, rows: the_table_rows , style: { border: :unicode } + end + end + end + + module Date + # Render datetime in IB format (zero padded "yyyymmdd 12:00:00") + + def to_ib timezone = 'UTC' + t = to_time + 12 * 60 * 60 # convert to time (noon) + s= "#{t.year}#{sprintf("%02d", t.month)}#{sprintf("%02d", t.day)}" + + if timezone == 'UTC' + s + "-#{sprintf("%02d", t.hour)}:#{sprintf("%02d", t.min)}:#{sprintf("%02d", t.sec)}" + else + s + " #{sprintf("%02d", t.hour)}:#{sprintf("%02d", t.min)}:#{sprintf("%02d", t.sec)} #{timezone}" + end + end + + end + + module Time + # Render datetime in IB format (zero padded "yyyymmdd HH:mm:ss") + # Without specifying the timezone utc is used + + def to_ib timezone = 'UTC' + s= "#{year}#{sprintf("%02d", month)}#{sprintf("%02d", day)}" + if timezone == 'UTC' + unless utc? + self.clone.utc.to_ib + else + s + "-#{sprintf("%02d", hour)}:#{sprintf("%02d", min)}:#{sprintf("%02d", sec)}" + end + else + s + " #{sprintf("%02d", hour)}:#{sprintf("%02d", min)}:#{sprintf("%02d", sec)} #{timezone}" + end + end + end + + module Numeric + # Conversion 0/1 into true/false + module Bool + def to_bool + self == 0 ? false : true + end + end + module Extensions + def blank? + false + end + end + end + + module BoolClass + # Conversion 0/1 into true/false + module Bool + def to_bool + self + end + end + module Extensions + def blank? + to_bool + end + end + end + module String + + module Extensions + def blank? + size > 0 + end + end + module Bool + def to_bool + case self.chomp.upcase + when 'TRUE', 'T', '1' + true + when 'FALSE', 'F', '0', '', Float::MAX + false + else + error "Unable to convert #{self} to bool" + end + end + end + end + module Symbol + module Float + def to_f + 0 + end + end + module Extensions + def blank? + false + end + end + + module Sort + # ActiveModel serialization depends on this method + def <=> other + to_s <=> other.to_s + end + end + end + module Object + # We still need to pass on nil, meaning: no value + def to_sup + self.to_s.upcase unless self.nil? + end + end + +end + +Array.include ClassExtensions::Array::DuplicatesCounter +Array.include ClassExtensions::Array::TablePresenter +FalseClass.include ClassExtensions::BoolClass::Bool +FalseClass.include ClassExtensions::BoolClass::Extensions +Date.include ClassExtensions::Date +NilClass.include ClassExtensions::BoolClass::Bool +NilClass.include ClassExtensions::BoolClass::Extensions +Numeric.include ClassExtensions::Numeric::Bool +Numeric.include ClassExtensions::Numeric::Extensions +Object.include ClassExtensions::Object +String.include ClassExtensions::String::Bool +String.include ClassExtensions::String::Extensions +Symbol.include ClassExtensions::Symbol::Float +Symbol.include ClassExtensions::Symbol::Sort +Symbol.include ClassExtensions::Symbol::Extensions +Time.include ClassExtensions::Time +TrueClass.include ClassExtensions::BoolClass::Bool +TrueClass.include ClassExtensions::BoolClass::Extensions + + + + + + + + +### Patching Object#error in ib/errors +# def error message, type=:standard + +### Patching Object#log, #default_logger= in ib/logger +# def default_logger +# def default_logger= logger +# def log *args diff --git a/lib/extensions/class-extensions.rb b/lib/extensions/class-extensions.rb deleted file mode 100644 index 7a8b1b3..0000000 --- a/lib/extensions/class-extensions.rb +++ /dev/null @@ -1,96 +0,0 @@ -module CoreExtensions - module Array - module DuplicatesCounter - def count_duplicates - self.each_with_object(Hash.new(0)) { |element, counter| counter[element] += 1 }.sort_by{|k,v| -v}.to_h - end - end - - module TablePresenter - def as_table(&b) - the_table_header = first.table_header(&b) - the_table_rows = map &:table_row - Terminal::Table.new headings: the_table_header, rows: the_table_rows , style: { border: :unicode } - end - end - end -end - -Array.include CoreExtensions::Array::DuplicatesCounter -Array.include CoreExtensions::Array::TablePresenter - - - - -class Time - # Render datetime in IB format (zero padded "yyyymmdd HH:mm:ss") - def to_ib - "#{year}#{sprintf("%02d", month)}#{sprintf("%02d", day)} " + - "#{sprintf("%02d", hour)}:#{sprintf("%02d", min)}:#{sprintf("%02d", sec)}" - end -end # Time - -class Numeric - # Conversion 0/1 into true/false - def to_bool - self == 0 ? false : true - end -end - -class TrueClass - def to_bool - self - end -end - -class FalseClass - def to_bool - self - end -end - -class String - def to_bool - case self.chomp.upcase - when 'TRUE', 'T', '1' - true - when 'FALSE', 'F', '0', '' - false - else - error "Unable to convert #{self} to bool" - end - end -end - -class NilClass - # We still need to pass on nil, meaning: no value - def to_bool - self - end -end - -class Symbol - def to_f - 0 - end - - # ActiveModel serialization depends on this method - def <=> other - to_s <=> other.to_s - end -end - -class Object - # We still need to pass on nil, meaning: no value - def to_sup - self.to_s.upcase unless self.nil? - end -end - -### Patching Object#error in ib/errors -# def error message, type=:standard - -### Patching Object#log, #default_logger= in ib/logger -# def default_logger -# def default_logger= logger -# def log *args diff --git a/lib/ib-api.rb b/lib/ib-api.rb index d0c2116..93bbb48 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -1,7 +1,44 @@ -module IB -end -IbRuby = IB -Ib = IB +require "zeitwerk" +require "active_model" +require 'active_support/concern' +require 'active_support/core_ext/module/attribute_accessors.rb' +require 'bigdecimal/util' # provides .to_d for numeric and string classes +require 'class_extensions' +require 'logger' +require 'terminal-table' +require 'workflow' -require 'requires' +#require 'ib/version' +#require 'ib/connection' + +require "server_versions" + +require 'ib/constants' +require 'ib/errors' +#loader = Zeitwerk::Loader.new +loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false) +loader.ignore("#{__dir__}/server_versions.rb") +loader.ignore("#{__dir__}/ib-api.rb") +loader.ignore("#{__dir__}/ib/contract.rb") +loader.ignore("#{__dir__}/ib/constants.rb") +loader.ignore("#{__dir__}/ib/errors.rb") +loader.ignore("#{__dir__}/ib/order_condition.rb") +loader.ignore("#{__dir__}/ib/messages/outgoing/old-place-order.rb") +loader.ignore("#{__dir__}/ib/messages/outgoing/new-place-order.rb") +#loader.ignore("#{__dir__}/models") +loader.inflector.inflect( + "ib" => "IB", + "receive_fa" => "ReceiveFA", + "tick_efp" => "TickEFP", + ) +#loader.push_dir("#{__dir__}") +loader.push_dir("#{__dir__}/../models/") +loader.push_dir("#{__dir__}/../conditions/") +loader.setup +loader.eager_load +#require 'requires' +require 'ib/contract.rb' +require 'ib/order_condition.rb' +#IbRuby = Ib +#IB = Ib diff --git a/lib/ib/base.rb b/lib/ib/base.rb index fc99cec..2f9840a 100644 --- a/lib/ib/base.rb +++ b/lib/ib/base.rb @@ -21,6 +21,12 @@ def initialize attributes={}, opts={} end end + + # define server version for any model class + def server_version + Connection.current &.server_version || 165 + end + # ActiveModel API (for serialization) def attributes @@ -60,7 +66,7 @@ def save alias save! save ### Noop methods mocking ActiveRecord::Base macros - + def self.attr_protected *args end diff --git a/lib/ib/base_properties.rb b/lib/ib/base_properties.rb index 2fc1a5f..3e354bc 100644 --- a/lib/ib/base_properties.rb +++ b/lib/ib/base_properties.rb @@ -1,5 +1,3 @@ -require 'active_model' -require 'active_support/concern' #require 'active_support/hash_with_indifferent_access' module IB @@ -33,14 +31,14 @@ def table_row #│ U7274612 │ Option: ESTX50 20211217 put 4200.0 DTB EUR │ -4 │ 179.85 │ 169.831 │ -6793.22 │ 400.78 │ │ #│ U7274612 │ Option: ESTX50 20211217 call 4200.0 DTB EUR │ -4 │ 97.85 │ 131.438 │ -5257.51 │ -1343.51 │ │ #└───────────┴─────────────────────────────────────────────┴─────┴────────┴─────────┴──────────┴────────────┴──────────┘ -## +# def as_table &b Terminal::Table.new headings: table_header(&b), rows: [table_row ], style: { border: :unicode } end # Comparison support def content_attributes - #NoMethodError if a Hash is assigned to an attribute + #NoMethodError if a Hash is assigned to an attribute Hash[attributes.reject do |(attr, _)| attr.to_s =~ /(_count)\z/ || [:created_at, :type, :updated_at, @@ -51,9 +49,9 @@ def content_attributes =begin Remove all Time-Stamps from the list of Attributes =end - def invariant_attributes - attributes.reject{|x| x =~ /_at/} - end + def invariant_attributes + attributes.reject{|x| x =~ /_at/} + end # Update nil attributes from given Hash or model def update_missing attrs @@ -134,8 +132,8 @@ def self.define_property_methods name, body={} body[:get] when body[:get] proc { self[name].send "to_#{body[:get]}" } - when VALUES[name] # property is encoded - proc { VALUES[name][self[name]] } + when IB::VALUES[name] # property is encoded + proc { IB::VALUES[name][self[name]] } else proc { self[name] } end diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 49853c6..78b13e1 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -1,10 +1,3 @@ -require 'thread' -#require 'active_support' -require 'ib/socket' -require 'logger' -require 'logging' -require 'ib/messages' - module IB # Encapsulates API connection to TWS or Gateway class Connection @@ -13,43 +6,94 @@ class Connection ## -------------------------------------------- Interface --------------------------------- ## public attributes: socket, next_local_id ( alias next_order_id) ## public methods: connect (alias open), disconnect, connected? - ## subscribe, unsubscribe - ## send_message (alias dispatch) - ## place_order, modify_order, cancel_order + ## subscribe, unsubscribe + ## send_message (alias dispatch) + ## place_order, modify_order, cancel_order ## public data-queue: received, received?, wait_for, clear_received - ## misc: reader_running? + ## misc: reader_running? - include Support::Logging # provides default_logger + include ::Support::Logging # provides default_logger + include Plugins + include Workflow mattr_accessor :current - # Please note, we are realizing only the most current TWS protocol versions, - # thus improving performance at the expense of backwards compatibility. - # Older protocol versions support can be found in older gem versions. attr_accessor :socket # Socket to IB server (TWS or Gateway) attr_accessor :next_local_id # Next valid order id attr_accessor :client_id attr_accessor :server_version attr_accessor :client_version + attr_accessor :host + attr_accessor :received + attr_accessor :port + attr_accessor :plugins alias next_order_id next_local_id alias next_order_id= next_local_id= - def initialize host: '127.0.0.1', - port: '4002', # IB Gateway connection (default --> demo) 4001: production - #:port => '7497', # TWS connection --> demo 7496: production - connect: true, # Connect at initialization - received: true, # Keep all received messages in a @received Hash - # redis: false, # future plans + def workflow_state + @workflow_state + end + + workflow do + state :virgin do + event :try_connection, transitions_to: :ready + event :activate_managed_accounts, transitions_to: :gateway_mode + event :collect_data, transitions_to: :lean_mode + end + + state :lean_mode do + event :try_connection, transitions_to: :ready + end + + state :gateway_mode do + event :try_connection, transitions_to: :ready + event :initialize_managed_accounts, transitions_to: :account_based_operations + end + state :ready do + event :initialize_managed_accounts, transitions_to: :account_based_operations # plugin managed_account + event :disconnect, transitions_to: :disconnected + end + state :disconnected do + event :try_connection, transitions_to: :ready + event :activate_managed_accounts, transitions_to: :gateway_mode # plugin managed_account + + end + + state :account_based_operations do + event :disconnect, transitions_to: :disconnected + event :initialize_order_handling, transitions_to: :account_based_orderflow # plugin process-orders + end + + state :account_based_orderflow do + event :disconnect, transitions_to: :disconnected + end + + on_transition do |from, to, triggering_event, *event_args| + logger.warn{ "Workflow:: #{workflow_state} -> #{to}" } + end + end + + + def initialize host: '127.0.0.1:4002', # combination of host + port + port: nil, + #:port => '7497', # TWS connection --> demo 7496: production + # connect: true, # Connect at initialization ---> disabled in favour of Connection.new.try_connection! + # received: true, # Keep all received messages in a @received Hash ---> disabled; automatically disabled in lean- and + # gateway-modus logger: nil, client_id: rand( 1001 .. 9999 ) , - client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb + client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb optional_capacities: "", # TWS-Version 974: "+PACEAPI" + plugins: [], #server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb **any_other_parameters_which_are_ignored # V 974 release motes # API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect. + Connection.current = self self.class.configure_logger logger + # enable specification of host and port through host: 'localhost:4002' as parameter + host, port = (host+':'+port.to_s).split(':') # convert parameters into instance-variables and assign them method(__method__).parameters.each do |type, k| next unless type == :key ## available: key , keyrest @@ -63,105 +107,152 @@ def initialize host: '127.0.0.1', @receive_lock = Mutex.new @message_lock = Mutex.new + @parser = nil @connected = false - self.next_local_id = nil - # TWS always sends NextValidId message at connect -subscribe save this id - self.subscribe(:NextValidId) do |msg| - self.logger.progname = "Connection#connect" - self.next_local_id = msg.local_id - self.logger.info { "Got next valid order id: #{next_local_id}." } + @plugins.each do |name| + activate_plugin name end + + @next_local_id = nil + # # this block is executed before tws-communication is established # Its intended for globally available subscriptions of tws-messages yield self if block_given? - if connect - disconnect if connected? - update_next_order_id - Kernel.exit if self.next_local_id.nil? # emergency exit. - # update_next_order_id should have raised an error - end - Connection.current = self end - # read actual order_id and - # connect if not connected - def update_next_order_id + # read actual order_id and + # connect if not connected + def update_next_order_id q = Queue.new subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id } - unless connected? - connect() # connect implies requesting NextValidId - else - send_message :RequestIds - end + try_connection! unless connected? + send_message :RequestIds th = Thread.new { sleep 5; q.close } - local_id = q.pop + @next_local_id = q.pop if q.closed? error "Could not get NextValidID", :reader else th.kill end unsubscribe subscription - local_id # return next_id + @next_local_id # return next_id end - ### Working with connection + ### Working with connection + def connected? + @connected + end # - ### connect can be called directly. but is mostly called through update_next_order_id - def connect - logger.progname='IB::Connection#connect' - if connected? - error "Already connected!" - return - end - - self.socket = IBSocket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible - socket.initialising_handshake - socket.decode_message( socket.recieve_messages ) do | the_message | - # logger.info{ "TheMessage :: #{the_message.inspect}" } - @server_version = the_message.shift.to_i - error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER - - @remote_connect_time = DateTime.parse the_message.shift - @local_connect_time = Time.now - end - - # V100 initial handshake - # Parameters borrowed from the python client - start_api = 71 - version = 2 - # optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities - socket.send_messages start_api, version, @client_id , @optional_capacities - @connected = true - logger.fatal{ "Connected to server, version: #{@server_version}, " + - "using client-id: #{client_id},\n connection time: " + - "#{@local_connect_time} local, " + - "#{@remote_connect_time} remote." } + ### Event – call through Connection-object.try_connection! + protected + def try_connection + logger.progname='IB::Connection#Event:TryConnection' + if connected? + error "Already connected!" + return + end + # TWS always sends NextValidId message at connect -subscribe save this id + subscribe(:NextValidId) do |msg| + logger.progname = "Connection" + @next_local_id = msg.local_id + logger.info { "Got next valid order id: #{@next_local_id}." } + end - start_reader - end + self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible + socket.initialising_handshake + @parser = RawMessageParser.new socket + @parser.each do | the_message | +# socket.decode_message( socket.receive_messages ) do | the_message | + #puts "TheMessage :: #{the_message.inspect}" + @server_version = the_message.shift.to_i.freeze + error "ServerVersion does not match #{@server_version} <--> #{MAX_CLIENT_VER}" if @server_version != MAX_CLIENT_VER + + @remote_connect_time = DateTime.parse the_message.shift.freeze + @local_connect_time = Time.now.freeze + @connected = true + break # only receive one message + end + + # V100 initial handshake + # Parameters borrowed from the python client + start_api = 71 + version = 2 + # optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities + socket.send_messages start_api, version, @client_id , @optional_capacities + logger.fatal{ "Connected to server, version: #{@server_version}, " + + "using client-id: #{client_id},\n connection time: " + + "#{@local_connect_time} local, " + + "#{@remote_connect_time} remote." } + start_reader + rescue IB::TransmissionError =>e + logger.fatal "Transmission Error: Retrying establishing the connection" + logger.fatal e.msg + disconnect! + try_connection! + # update_next_order_id + end - alias open connect # Legacy alias + ### Event – call through Connection-object.disconnect! def disconnect if reader_running? @reader_running = false @reader_thread.join end - if connected? - socket.close - @connected = false - end + socket.close + @connected = false end - alias close disconnect # Legacy alias - def connected? - @connected + public + + # disconnect and restart communication with the tws. + # + # cancels all subscriptions and reestablishes standard + # subscriptions for the current workflow state. + # + # connects if called in the disconnected state + # + # + # Usecase: + # 3.2.0 :015 > Symbols::Stocks.msft.verify + # A: Error reading request. Unable to parse data. java.lang.NumberFormatException: For input string: "MSFT" + # + # ^C/usr/share/rvm/rubies/ruby-3.2.0/lib/ruby/3.2.0/irb.rb:438:in `raise': abort then interrupt! (IRB::Abort) + # from :18:in `pop' + # from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:164:in `_verify' + # from /home/ubuntu/labor/ib-api/plugins/ib/verify.rb:78:in `verify' + # from (irb):15:in `
' + # from ./console:96:in `
' + # 3.2.0 :016 > C.reconnect + # F: Connected to server, version: 165, using client-id: 2000, + # connection time: 2024-11-21 20:57:57 +0100 local, 2024-11-21T19:57:57+00:00 remote. + # => 227896 + # 3.2.0 :017 > Symbols::Stocks.msft.verify + # => [] + # + def reconnect + return if workflow_state == "virgin" + old_workflowstate = workflow_state.dup + disconnect! unless disconnected? + + unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq + + if ["ready", "lean_mode", "disconnected"].include? old_workflowstate + try_connection! + else + activate_managed_accounts! + unless old_workflowstate == 'gateway_mode' + initialize_managed_accounts! + initialize_order_handling! unless old_workflowstate != "account_based_orderflow" + end + end end + ### Working with message subscribers # Subscribe Proc or block to specific type(s) of incoming message events. @@ -172,20 +263,20 @@ def subscribe *args, &block subscriber = args.last.respond_to?(:call) ? args.pop : block id = random_id - error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc + error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc args.each do |what| message_classes = case - when what.is_a?(Class) && what < Messages::Incoming::AbstractMessage + when what.is_a?(Class) && what < IB::Messages::Incoming::AbstractMessage [what] when what.is_a?(Symbol) - if Messages::Incoming.const_defined?(what) - [Messages::Incoming.const_get(what)] - elsif TechnicalAnalysis::Signals.const_defined?(what) - [TechnicalAnalysis::Signals.const_get?(what)] + if IB::Messages::Incoming.const_defined?(what) + [IB::Messages::Incoming.const_get(what)] + # elsif TechnicalAnalysis::Signals.const_defined?(what) + # [TechnicalAnalysis::Signals.const_get?(what)] else - error "#{what} is no IB::Messages or TechnicalAnalyis::Signals class" + error "#{what} is no IB::Messages class" end when what.is_a?(Regexp) Messages::Incoming::Classes.values.find_all { |klass| klass.to_s =~ what } @@ -205,17 +296,18 @@ def subscribe *args, &block end # Remove all subscribers with specific subscriber id - def unsubscribe *ids - @subscribe_lock.synchronize do - ids.collect do |id| - removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact - logger.error "No subscribers with id #{id}" if removed_at_id.empty? - removed_at_id # return_value - end.flatten - end - end - ### Working with received messages Hash + def unsubscribe *ids + @subscribe_lock.synchronize do + ids.collect do |id| + removed_at_id = subscribers.map { |_, subscribers| subscribers.delete id }.compact + logger.error "No subscribers with id #{id}" if removed_at_id.empty? + removed_at_id # return_value + end.flatten + end + end + + ### Working with received messages Hash # Clear received messages Hash def clear_received *message_types @receive_lock.synchronize do @@ -227,19 +319,21 @@ def clear_received *message_types end end + + # Hash of received messages, keyed by message type def received @received_hash ||= Hash.new do |hash, message_type| - # enable access to the hash via - # ib.received[:MessageType].attribute - the_array = Array.new - def the_array.method_missing(method, *key) - unless method == :to_hash || method == :to_str #|| method == :to_int - return self.map{|x| x.public_send(method, *key)} - end - end - hash[message_type] = the_array - end + # enable access to the hash via + # ib.received[:MessageType].attribute + the_array = Array.new + def the_array.method_missing(method, *key) + unless method == :to_hash || method == :to_str #|| method == :to_int + return self.map{|x| x.public_send(method, *key)} + end + end + hash[message_type] = the_array + end end # Check if messages of given type were received at_least n times @@ -253,9 +347,8 @@ def received? message_type, times=1 # Wait for specific condition(s) - given as callable/block, or # message type(s) - given as Symbol or [Symbol, times] pair. # Timeout after given time or 1 second. - # - # wait_for depends heavyly on Connection#received. If collection of messages through recieved - # is turned off, wait_for loses most of its functionality + # wait_for depends on Connection#received. If collection of messages through recieved + # is turned off, wait_for loses most of its functionality def wait_for *args, &block timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args @@ -274,6 +367,7 @@ def wait_for *args, &block ### Working with Incoming messages from IB + protected def reader_running? @reader_running && @reader_thread && @reader_thread.alive? end @@ -284,30 +378,32 @@ def process_messages poll_time = 50 # in msec begin while (time_left = time_out - Time.now) > 0 # If socket is readable, process single incoming message - #process_message if select [socket], nil, nil, time_left - # the following checks for shutdown of TWS side; ensures we don't run in a spin loop. - # unfortunately, it raises Errors in windows environment - if select [socket], nil, nil, time_left - # # Peek at the message from the socket; if it's blank then the - # # server side of connection (TWS) has likely shut down. - socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == "" - # - # # We go ahead process messages regardless (a no-op if socket_likely_shutdown). - process_message - # - # # After processing, if socket has shut down we sleep for 100ms - # # to avoid spinning in a tight loop. If the server side somehow - # # comes back up (gets reconnedted), normal processing - # # (without the 100ms wait) should happen. - sleep(0.1) if socket_likely_shutdown - end - end + # windows environment: just read the socket + if RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/) + process_message if select [socket], nil, nil, time_left + else + # the following checks for shutdown of TWS side; ensures we don't run in a spin loop. + # unfortunately, it raises Errors in windows environment + if select [socket], nil, nil, time_left + # Peek at the message from the socket; if it's blank then the + # server side of connection (TWS) has likely shut down. + socket_likely_shutdown = socket.recvmsg(100, ::Socket::MSG_PEEK)[0] == "" + # We go ahead process messages regardless (a no-op if socket_likely_shutdown). + process_message + # After processing, if socket has shut down we sleep for 100ms + # to avoid spinning in a tight loop. If the server side somehow + # comes back up (gets reconnedted), normal processing + # (without the 100ms wait) should happen. + sleep(0.1) if socket_likely_shutdown + end # if + end # if + end # while rescue Errno::ECONNRESET => e logger.fatal e.message if e.message =~ /Connection reset by peer/ logger.fatal "Is another client listening on the same port?" error "try reconnecting with a different client-id", :reader - else + else logger.fatal "Aborting" Kernel.exit end @@ -317,7 +413,8 @@ def process_messages poll_time = 50 # in msec ### Sending Outgoing messages to IB # Send an outgoing message. - # returns the used request_id if appropiate, otherwise "true" + # returns the used request_id if appropiate, otherwise "true" + public def send_message what, *args message = case @@ -331,18 +428,17 @@ def send_message what, *args error "Only able to send outgoing IB messages", :args end error "Not able to send messages, IB not connected!" unless connected? - begin + begin @message_lock.synchronize do message.send_to socket end - rescue Errno::EPIPE - logger.error{ "Broken Pipe, trying to reconnect" } - disconnect - connect - retry - end - ## return the transmitted message - message.data[:request_id].presence || true + rescue Errno::EPIPE + logger.error{ "Broken Pipe, trying to reconnect" } + reconnect + retry + end + ## return the transmitted message + message.data[:request_id].presence || true end alias dispatch send_message # Legacy alias @@ -351,23 +447,23 @@ def send_message what, *args # Assigns client_id and order_id fields to placed order. Returns assigned order_id. def place_order order, contract # order.place contract, self ## old - error "Unable to place order, next_local_id not known" unless next_local_id - error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil? + error "Unable to place order, next_local_id not known" unless @next_local_id + error "local_id present. Order is already placed. Do might use modify insteed" unless order.local_id.nil? order.client_id = client_id - order.local_id = next_local_id - self.next_local_id += 1 + order.local_id = @next_local_id + @next_local_id += 1 order.placed_at = Time.now - modify_order order, contract + modify_order order, contract end # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id. def modify_order order, contract - # order.modify contract, self ## old - error "Unable to modify order; local_id not specified" if order.local_id.nil? + error "Unable to modify order; local_id not specified" if order.local_id.nil? order.modified_at = Time.now + # if con_id is present, to place an order use only con_id and exchange send_message :PlaceOrder, - :order => order, - :contract => contract, + :order => order.then{|y| y.contract = nil; y}, + :contract => contract.con_id.to_i > 0 ? Contract.new( con_id: contract.con_id, exchange: contract.exchange || 'SMART' ) : contract, :local_id => order.local_id order.local_id # return value end @@ -382,85 +478,96 @@ def cancel_order *local_ids # Start reader thread that continuously reads messages from @socket in background. # If you don't start reader, you should manually poll @socket for messages # or use #process_messages(msec) API. + protected def start_reader if @reader_running @reader_thread - elsif connected? + else # connected? # if called from try_connection, the connected state is not set begin Thread.abort_on_exception = true @reader_running = true @reader_thread = Thread.new { process_messages while @reader_running } rescue Errno::ECONNRESET => e logger.fatal e.message - Kernel.exit + reconnect + end +# else +# error "Could not start reader, not connected!", :reader, true + end + end + + # Message subscribers. Key is the message class to listen for. + # Value is a Hash of subscriber Procs, keyed by their subscription id. + # All subscriber Procs will be called with the message instance + # as an argument when a message of that type is received. + def subscribers + @subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new } + end + + # Process single incoming message (blocking!) + def process_message + logger.progname='IB::Connection#process_message' + + ## decode mesage is included throught `prepare_data +# socket.decode_message( socket.receive_messages ) do | the_decoded_message | + @parser.each do | the_decoded_message | + # puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}" + msg_id = the_decoded_message.shift.to_i + + # Debug: + # logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"} + + # Create new instance of the appropriate message type, + # and have it read the message from socket. + # NB: Failure here usually means unsupported message type received + + ## raising IB::TransmissionError if something went wrong. + ## the calling program has to initiate reconnection + logger.fatal { the_decoded_message } unless Messages::Incoming::Classes[msg_id] + error "Got unsupported message #{msg_id}", :reader unless Messages::Incoming::Classes[msg_id] + error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero? + begin + msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message) + rescue IB::TransmissionError + logger.fatal { the_decoded_message } + raise + end + + # Deliver message to all registered subscribers, alert if no subscribers + # Ruby 2.0 and above: Hashes are ordered. + # Thus first declared subscribers of a class are executed first + @subscribe_lock.synchronize do + subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) } + end + logger.info { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty? + + # Collect all received messages into a @received Hash + if @received + @receive_lock.synchronize do + received[msg.message_type] << msg + end end - else - error "Could not start reader, not connected!", :reader, true end end - protected - # Message subscribers. Key is the message class to listen for. - # Value is a Hash of subscriber Procs, keyed by their subscription id. - # All subscriber Procs will be called with the message instance - # as an argument when a message of that type is received. - def subscribers - @subscribers ||= Hash.new { |hash, subs| hash[subs] = Hash.new } - end - - # Process single incoming message (blocking!) - def process_message - logger.progname='IB::Connection#process_message' if logger.is_a?(Logger) - - socket.decode_message( socket.recieve_messages ) do | the_decoded_message | - # puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}" - msg_id = the_decoded_message.shift.to_i - - # Debug: - # logger.debug { "Got message #{msg_id} (#{Messages::Incoming::Classes[msg_id]})"} - - # Create new instance of the appropriate message type, - # and have it read the message from socket. - # NB: Failure here usually means unsupported message type received - logger.error { "Got unsupported message #{msg_id}" } unless Messages::Incoming::Classes[msg_id] - error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero? - msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message) - - # Deliver message to all registered subscribers, alert if no subscribers - # Ruby 2.0 and above: Hashes are ordered. - # Thus first declared subscribers of a class are executed first - @subscribe_lock.synchronize do - subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) } - end - logger.info { "No subscribers for message #{msg.class}!" } if subscribers[msg.class].empty? - - # Collect all received messages into a @received Hash - if @received - @receive_lock.synchronize do - received[msg.message_type] << msg - end - end - end - end - - def random_id - rand 999999 - end - - # Check if all given conditions are satisfied - def satisfied? *conditions - !conditions.empty? && - conditions.inject(true) do |result, condition| - result && if condition.is_a?(Symbol) - received?(condition) - elsif condition.is_a?(Array) - received?(*condition) - elsif condition.respond_to?(:call) - condition.call - else - logger.error { "Unknown wait condition #{condition}" } - end - end - end + def random_id + rand 999999 + end + + # Check if all given conditions are satisfied + def satisfied? *conditions + !conditions.empty? && + conditions.inject(true) do |result, condition| + result && if condition.is_a?(Symbol) + received?(condition) + elsif condition.is_a?(Array) + received?(*condition) + elsif condition.respond_to?(:call) + condition.call + else + logger.error { "Unknown wait condition #{condition}" } + end + end + end end # class Connection end # module IB diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index 1685451..68a767e 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -1,4 +1,5 @@ module IB + ### Widely used TWS constants: EOL = "\0" @@ -104,7 +105,7 @@ module IB 55 => :trade_rate, # tickGeneric() 56 => :volume_rate, # tickGeneric() 57 => :last_rth_trade, # - 58 => :rt_historical_vol, + 58 => :rt_historical_vol, 59 => :ib_dividends, 60 => :bond_factor_multiplier, 61 => :regulatory_imbalance, @@ -135,9 +136,9 @@ module IB 86 => :futures_open_interest, 87 => :avg_opt_volume, 88 => :not_set, - 105 => :average_option_volume #(for Stocks) tickGeneric() + 105 => :average_option_volume #(for Stocks) tickGeneric() - + # Note 1: Tick types BID_OPTION, ASK_OPTION, LAST_OPTION, and MODEL_OPTION return # all Greeks (delta, gamma, vega, theta), the underlying price and the # stock and option reference price when requested. @@ -161,8 +162,8 @@ module IB 0 => :unknown, 1 => :real_time, 2 => :frozen, - 3 => :delayed, - 4 => :frozen_delayed }.freeze + 3 => :delayed, + 4 => :frozen_delayed }.freeze # Market depth messages contain these "operation" codes to tell you what to do with the data. # See also http://www.interactivebrokers.com/php/apiUsersGuide/apiguide/java/updatemktdepth.htm @@ -191,17 +192,18 @@ module IB 'QUOTE' => :request_for_quote, # Request for Quote 'STP' => :stop, # Stop 'STP LMT' => :stop_limit, # Stop Limit - 'STP PRT' => :stop_protected, # Stop with Protection + 'STP PRT' => :stop_protected, # Stop with Protection 'TRAIL' => :trailing_stop, # Trailing Stop 'TRAIL LIMIT' => :trailing_limit, # Trailing Stop Limit 'TRAIL LIT' => :trailing_limit_if_touched, # Trailing Limit if Touched 'TRAIL MIT' => :trailing_market_if_touched, # Trailing Market If Touched - 'REL' => :relative, # Relative + 'REL' => :pegged_to_primary , # Relative aka Pegged to Primary 'BOX TOP' => :box_top, # Box Top 'PEG MKT' => :pegged_to_market, # Pegged-to-Market 'PEG STK' => :pegged_to_market, # Pegged-to-Stock 'PEG MID' => :pegged_to_midpoint, # Pegged-to-Midpoint - 'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benmchmark # Vers. 102 + 'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benchmark # Vers. 102 + 'PEG BEST' => :pegged_to_best, 'VWAP' => :vwap, # VWAP-Guaranted 'VOL' => :volatility, # Volatility 'SCALE' => :scale, # Scale @@ -210,7 +212,7 @@ module IB }.freeze # Valid security types (sec_type attribute of IB::Contract) SECURITY_TYPES = - { 'BAG' => :bag, + { 'BAG' => :bag, 'BOND' => :bond, 'CASH' => :forex, 'CMDTY'=> :commodity, @@ -226,12 +228,16 @@ module IB 'IOPT' => :dutch_option, 'STK' => :stock, 'WAR' => :warrant, - 'ICU' => :icu, - 'ICS' => :ics, - 'BILL' => :bill, - 'BSK' => :basket, - 'FWD' => :forward, - 'FIXED' => :fixed }.freeze + 'ICU' => :icu, + 'ICS' => :ics, + 'BILL' => :bill, + 'BSK' => :basket, + 'FWD' => :forward, + 'FIXED' => :fixed , + 'CRYPTO' => :crypto, + "EC" => :event_contract # +# "Event Contracts are daily-expiring, cash settled, European Style, binary-options on futures contracts, offering short-term trading opportunities for individuals seeking to take a position on daily price moves on futures using smaller-value trades of up to $20 per contract. The Event Contracts allow market participants to trade their view on the price direction of key futures markets at the end of each day’s trading session." + }.freeze # Obtain symbolic value from given property code: # VALUES[:side]['B'] -> :buy @@ -320,8 +326,8 @@ module IB 'GTD' => :good_till_date, 'GTC' => :good_till_cancelled, 'IOC' => :immediate_or_cancel, - 'OPG' => :opening_price, - 'AUC' => :at_auction}, + 'OPG' => :opening_price, + 'AUC' => :at_auction}, :rule_80a => {'I' => :individual, @@ -343,9 +349,9 @@ module IB 'n' => :farmm, 'y' => :specialist}, # conditions - :conjunction_connection => { 'o' => :or, 'a' => :and }, - :operator => { 1 => '>=' , 0 => '<=' } - + :conjunction_connection => { 'o' => :or, 'a' => :and }, + :operator => { 1 => '>=' , 0 => '<=' } + }.freeze # Obtain property code from given symbolic value: diff --git a/lib/ib/contract.rb b/lib/ib/contract.rb new file mode 100644 index 0000000..e0dce8c --- /dev/null +++ b/lib/ib/contract.rb @@ -0,0 +1,30 @@ +module IB + + # Here we reopen IB::Contract and implenent the dynamic build facility + # This file is required after zeitwerk processed the basic includes. + # + class Contract + # Contract subclasses representing specialized security types. + using IB::Support + + Subclasses = Hash.new(Contract) + Subclasses[:bag] = IB::Bag + Subclasses[:option] = IB::Option + Subclasses[:futures_option] = IB::FutureOption + Subclasses[:future] = IB::Future + Subclasses[:stock] = IB::Stock + Subclasses[:forex] = IB::Forex + Subclasses[:index] = IB::Index + + + # This builds an appropriate Contract subclass based on its type + # + # the method is also used to copy Contract.values to new instances + def self.build opts = {} + subclass =( VALUES[:sec_type][opts[:sec_type]] || opts['sec_type'] || opts[:sec_type]).to_sym + Contract::Subclasses[subclass].new opts + end + + + end # class Contract +end diff --git a/lib/ib/errors.rb b/lib/ib/errors.rb index 2774d25..bf1e40c 100644 --- a/lib/ib/errors.rb +++ b/lib/ib/errors.rb @@ -18,6 +18,9 @@ class FlexError < RuntimeError class TransmissionError < RuntimeError end + # define a custom ErrorClass which can be fired if a verification fails + class VerifyError < StandardError + end end # module IB # Patching Object with universally accessible top level error method. diff --git a/lib/ib/messages.rb b/lib/ib/messages.rb index ad1fa90..59e2db0 100644 --- a/lib/ib/messages.rb +++ b/lib/ib/messages.rb @@ -1,16 +1,56 @@ -require 'ib/server_versions' +require 'server_versions' module IB module Messages - # This gem supports incoming/outgoing IB messages compatible with the following - # IB client/server versions: + # This gem supports incoming/outgoing IB messages compatible with the following + # IB client/server versions: CLIENT_VERSION = 66 # => API V 9.71 - SERVER_VERSION = "v"+ MIN_CLIENT_VER.to_s + ".." + MAX_CLIENT_VER.to_s # extracted from the python-client + SERVER_VERSION = "v"+ MIN_CLIENT_VER.to_s + ".." + MAX_CLIENT_VER.to_s # extracted from the python-client + + + + # Macro that defines short message classes using a one-liner. + # First arg is either a [message_id, version] pair or just message_id (version 1) + # data_map contains instructions for processing @data Hash. Format: + # Incoming messages: [field, type] or [group, field, type] + # Outgoing messages: field, [field, default] or [field, method, [args]] + def def_message message_id_version, *data_map, &to_human + base = data_map.first.is_a?(Class) ? data_map.shift : self::AbstractMessage + message_id, version = message_id_version + + # Define new message class + message_class = Class.new(base) do + @message_id, @version = message_id, version || 1 + @data_map = data_map + @given_arguments =[] + + @data_map.each do |(name, _, type_args)| + dont_process = name == :request_id # [ :request_id, :local_id, :id ].include? name.to_sym + @given_arguments << name.to_sym + # Avoid redefining existing accessor methods + unless instance_methods.include?(name.to_s) || instance_methods.include?(name.to_sym) || dont_process + if type_args.is_a?(Symbol) # This is Incoming with [group, field, type] + attr_reader name + else + define_method(name) { @data[name] } + end + end + end + + define_method(:to_human, &to_human) if to_human + end + + # Add defined message class to Classes Hash keyed by its message_id + self::Classes[message_id] = message_class + + message_class + end + end end -require 'ib/messages/outgoing' -require 'ib/messages/incoming' +#require 'ib/messages/outgoing' +#require 'ib/messages/incoming' __END__ // Client version history @@ -96,4 +136,4 @@ module Messages // 64 = can receive solicited attrib in openOrder message // 65 = can receive verifyAndAuthMessageAPI and verifyAndAuthCompleted messages // 66 = can receive randomize size and randomize price order fields - + // not updated since Vers. 9.71 diff --git a/lib/ib/messages/abstract_message.rb b/lib/ib/messages/abstract_message.rb index 74423f5..169f501 100644 --- a/lib/ib/messages/abstract_message.rb +++ b/lib/ib/messages/abstract_message.rb @@ -21,6 +21,10 @@ def self.version # Per class, minimum message version supported @version || 1 end + # including server-version as method to every message class + def server_version + Connection.current &.server_version || 165 + end def self.message_id @message_id end @@ -34,9 +38,9 @@ def message_id self.class.message_id end - def request_id - @data[:request_id].presence || nil - end + def request_id + @data[:request_id].presence || nil + end def message_type self.class.message_type @@ -44,9 +48,9 @@ def message_type attr_accessor :created_at, :data - def self.properties? - @given_arguments - end + def self.properties? + @given_arguments + end def to_human @@ -60,42 +64,5 @@ def to_human end # class AbstractMessage - # Macro that defines short message classes using a one-liner. - # First arg is either a [message_id, version] pair or just message_id (version 1) - # data_map contains instructions for processing @data Hash. Format: - # Incoming messages: [field, type] or [group, field, type] - # Outgoing messages: field, [field, default] or [field, method, [args]] - def def_message message_id_version, *data_map, &to_human - base = data_map.first.is_a?(Class) ? data_map.shift : self::AbstractMessage - message_id, version = message_id_version - - # Define new message class - message_class = Class.new(base) do - @message_id, @version = message_id, version || 1 - @data_map = data_map - @given_arguments =[] - - @data_map.each do |(name, _, type_args)| - dont_process = name == :request_id # [ :request_id, :local_id, :id ].include? name.to_sym - @given_arguments << name.to_sym - # Avoid redefining existing accessor methods - unless instance_methods.include?(name.to_s) || instance_methods.include?(name.to_sym) || dont_process - if type_args.is_a?(Symbol) # This is Incoming with [group, field, type] - attr_reader name - else - define_method(name) { @data[name] } - end - end - end - - define_method(:to_human, &to_human) if to_human - end - - # Add defined message class to Classes Hash keyed by its message_id - self::Classes[message_id] = message_class - - message_class - end - end # module Messages end # module IB diff --git a/lib/ib/messages/incoming.rb b/lib/ib/messages/incoming.rb index 66a57b5..e720233 100644 --- a/lib/ib/messages/incoming.rb +++ b/lib/ib/messages/incoming.rb @@ -1,4 +1,4 @@ -require 'ib/messages/incoming/abstract_message' +#require 'ib/messages/incoming/abstract_message' # EClientSocket.java uses sendMax() rather than send() for a number of these. # It sends an EOL rather than a number if the value == Integer.MAX_VALUE (or Double.MAX_VALUE). @@ -32,46 +32,46 @@ module Incoming # subscription can have (for outgoing RequestScannerSubscription message). ScannerParameters = def_message 19, [:xml, :xml] - class ScannerParameters - # returns a List of Hashes specifing Instruments. - # > C.received[:ScannerParameters].first.instruments.first - # => {:name=>"US Stocks", - # :type=>"STK", - # :filters=>"AFTERHRSCHANGEPERC,AVGOPTVOLUME,AVGPRICETARGET,AVGRATING,AVGTARGET2PRICERATIO,AVGVOLUME,AVGVOLUME_USD,CHANGEOPENPERC,CHANGEPERC,EMA_20,EMA_50,EMA_100,EMA_200,PRICE_VS_EMA_20,PRICE_VS_EMA_50,PRICE_VS_EMA_100,PRICE_VS_EMA_200,DAYSTOCOVER,DIVIB,DIVYIELD,DIVYIELDIB,FEERATE,FIRSTTRADEDATE,GROWTHRATE,HALTED,HASOPTIONS,HISTDIVIB,HISTDIVYIELDIB,IMBALANCE,IMBALANCEADVRATIOPERC,IMPVOLAT,IMPVOLATOVERHIST,INSIDEROFFLOATPERC,INSTITUTIONALOFFLOATPERC,MACD,MACD_SIGNAL,MACD_HISTOGRAM,MKTCAP,MKTCAP_USD,NEXTDIVAMOUNT,NEXTDIVDATE,NUMPRICETARGETS,NUMRATINGS,NUMSHARESINSIDER,NUMSHARESINSTITUTIONAL,NUMSHARESSHORT,OPENGAPPERC,OPTVOLUME,OPTVOLUMEPCRATIO,PERATIO,PILOT,PPO,PPO_SIGNAL,PPO_HISTOGRAM,PRICE,PRICE2BK,PRICE2TANBK,PRICERANGE,PRICE_USD,QUICKRATIO,REBATERATE,REGIMBALANCE,REGIMBALANCEADVRATIOPERC,RETEQUITY,SHORTABLESHARES,SHORTOFFLOATPERC,SHORTSALERESTRICTED,SIC,ISSUER_COUNTRY_CODE,SOCSACT,SOCSNET,STKTYPE,STVOLUME_3MIN,STVOLUME_5MIN,STVOLUME_10MIN,TRADECOUNT,TRADERATE,UNSHORTABLE,VOLUME,VOLUMERATE,VOLUME_USD,RCGLTCLASS,RCGLTENDDATE,RCGLTIVALUE,RCGLTTRADE,RCGITCLASS,RCGITENDDATE,RCGITIVALUE,RCGITTRADE,RCGSTCLASS,RCGSTENDDATE,RCGSTIVALUE,RCGSTTRADE", - # :group=>"STK.GLOBAL", - # :shortName=>"US", - # :cloudScanNotSupported=>"false"} - def instruments - @data[:xml][:ScanParameterResponse][:InstrumentList].first[:Instrument] - end - - # returns a List of Hashes specifing ScanTypes - # > C.received[:ScannerParameters].first.scan_types.first - # => {:displayName=>"Assets Under Management (AltaVista) Desc", - # :scanCode=>"SCAN_etfAssets_DESC", - # :instruments=>"ETF.EQ.US,ETF.FI.US", - # :absoluteColumns=>"false", - # :Columns=>{:ColumnSetRef=>{:colId=>"0", :name=>"PctPerf", :display=>"false", :displayType=>"DATA"}, - # :Column=>{:colId=>"6031", :name=>"Assets Under Management", :display=>"true", :displayType=>"DATA"}}, - # :supportsSorting=>"true", - # :respSizeLimit=>"2147483647", :snapshotSizeLimit=>"2147483647", - # :searchDefault=>"false", :access=>"unrestricted"} -# - - def scan_types - @data[:xml][:ScanParameterResponse][:ScanTypeList][:ScanType] - end - end + class ScannerParameters + # returns a List of Hashes specifing Instruments. + # > C.received[:ScannerParameters].first.instruments.first + # => {:name=>"US Stocks", + # :type=>"STK", + # :filters=>"AFTERHRSCHANGEPERC,AVGOPTVOLUME,AVGPRICETARGET,AVGRATING,AVGTARGET2PRICERATIO,AVGVOLUME,AVGVOLUME_USD,CHANGEOPENPERC,CHANGEPERC,EMA_20,EMA_50,EMA_100,EMA_200,PRICE_VS_EMA_20,PRICE_VS_EMA_50,PRICE_VS_EMA_100,PRICE_VS_EMA_200,DAYSTOCOVER,DIVIB,DIVYIELD,DIVYIELDIB,FEERATE,FIRSTTRADEDATE,GROWTHRATE,HALTED,HASOPTIONS,HISTDIVIB,HISTDIVYIELDIB,IMBALANCE,IMBALANCEADVRATIOPERC,IMPVOLAT,IMPVOLATOVERHIST,INSIDEROFFLOATPERC,INSTITUTIONALOFFLOATPERC,MACD,MACD_SIGNAL,MACD_HISTOGRAM,MKTCAP,MKTCAP_USD,NEXTDIVAMOUNT,NEXTDIVDATE,NUMPRICETARGETS,NUMRATINGS,NUMSHARESINSIDER,NUMSHARESINSTITUTIONAL,NUMSHARESSHORT,OPENGAPPERC,OPTVOLUME,OPTVOLUMEPCRATIO,PERATIO,PILOT,PPO,PPO_SIGNAL,PPO_HISTOGRAM,PRICE,PRICE2BK,PRICE2TANBK,PRICERANGE,PRICE_USD,QUICKRATIO,REBATERATE,REGIMBALANCE,REGIMBALANCEADVRATIOPERC,RETEQUITY,SHORTABLESHARES,SHORTOFFLOATPERC,SHORTSALERESTRICTED,SIC,ISSUER_COUNTRY_CODE,SOCSACT,SOCSNET,STKTYPE,STVOLUME_3MIN,STVOLUME_5MIN,STVOLUME_10MIN,TRADECOUNT,TRADERATE,UNSHORTABLE,VOLUME,VOLUMERATE,VOLUME_USD,RCGLTCLASS,RCGLTENDDATE,RCGLTIVALUE,RCGLTTRADE,RCGITCLASS,RCGITENDDATE,RCGITIVALUE,RCGITTRADE,RCGSTCLASS,RCGSTENDDATE,RCGSTIVALUE,RCGSTTRADE", + # :group=>"STK.GLOBAL", + # :shortName=>"US", + # :cloudScanNotSupported=>"false"} + def instruments + @data[:xml][:ScanParameterResponse][:InstrumentList].first[:Instrument] + end + + # returns a List of Hashes specifing ScanTypes + # > C.received[:ScannerParameters].first.scan_types.first + # => {:displayName=>"Assets Under Management (AltaVista) Desc", + # :scanCode=>"SCAN_etfAssets_DESC", + # :instruments=>"ETF.EQ.US,ETF.FI.US", + # :absoluteColumns=>"false", + # :Columns=>{:ColumnSetRef=>{:colId=>"0", :name=>"PctPerf", :display=>"false", :displayType=>"DATA"}, + # :Column=>{:colId=>"6031", :name=>"Assets Under Management", :display=>"true", :displayType=>"DATA"}}, + # :supportsSorting=>"true", + # :respSizeLimit=>"2147483647", :snapshotSizeLimit=>"2147483647", + # :searchDefault=>"false", :access=>"unrestricted"} +# + + def scan_types + @data[:xml][:ScanParameterResponse][:ScanTypeList][:ScanType] + end + end # Receives the current system time on the server side. CurrentTime = def_message 49, [:time, :int] # long! - HeadTimeStamp = def_message( [88, 0], [:request_id, :int], [:date, :int_date] ) do - # def to_human - "<#{self.message_type}:" + - "Request #{request_id}, First Historical Datapoint @ #{date.to_s}«" - end + HeadTimeStamp = def_message( [88, 0], [:request_id, :int], [:date, :int_date] ) do + # def to_human + "<#{self.message_type}:" + + "Request #{request_id}, First Historical Datapoint @ #{date.to_s}«" + end # Receive Reuters global fundamental market data. There must be a subscription to # Reuters Fundamental set up in Account Management before you can receive this data. @@ -86,9 +86,9 @@ def scan_types ExecutionDataEnd = def_message 55, [:request_id, :int] MarketDataType = def_message 58, [:request_id, :int], [:market_data_type, :int] do - "<#{self.message_type}:" + - " switched to »#{MARKET_DATA_TYPES[market_data_type]}«" # to_human - end + "<#{self.message_type}:" + + " switched to »#{MARKET_DATA_TYPES[market_data_type]}«" # to_human + end CommissionReport = def_message 59, [:exec_id, :string], @@ -98,71 +98,105 @@ def scan_types [:yield, :decimal_max], [:yield_redemption_date, :int] # YYYYMMDD format - SecurityDefinitionOptionParameter = OptionChainDefinition = def_message [75,0] , - [:request_id, :int], - [:exchange, :string], - [:con_id, :int], # underlying_con_id - [:trading_class, :string], - [:multiplier, :int] - - class OptionChainDefinition - using IBSupport # defines tws-method for Array (socket.rb) - def load - super - load_map [:expirations, :array, proc { @buffer.read_date }], - [:strikes, :array, proc { @buffer.read_decimal } ] - end - def expirations - @data[:expirations] - end - def strikes - @data[:strikes] - end - - def to_human - "OptionChainDefinition #{trading_class}@#{exchange} [#{multiplier} X ] strikes: #{strikes.first} - #{strikes.last} expirations: #{expirations.first} - #{expirations.last}" - end - end - - - - OptionChainDefinitionEnd = SecurityDefinitionOptionParameterEnd = def_message [76,0 ], - [ :request_id, :int ] + SecurityDefinitionOptionParameter = OptionChainDefinition = def_message [75,0] , + [:request_id, :int], + [:exchange, :string], + [:con_id, :int], # underlying_con_id + [:trading_class, :string], + [:multiplier, :int] + + class OptionChainDefinition + using IB::Support # defines tws-method for Array (socket.rb) + def load + super + load_map [:expirations, :array, proc { @buffer.read_date }], + [:strikes, :array, proc { @buffer.read_decimal } ] + end + def expirations + @data[:expirations] + end + def strikes + @data[:strikes] + end + + def to_human + "OptionChainDefinition #{trading_class}@#{exchange} [#{multiplier} X ] strikes: #{strikes.first} - #{strikes.last} expirations: #{expirations.first} - #{expirations.last}" + end + end + + OptionChainDefinitionEnd = SecurityDefinitionOptionParameterEnd = def_message [76,0 ], + [ :request_id, :int ] #<- 1-9-789--USD-CASH-----IDEALPRO--CAD------ #-> ---81-123-5.0E-5--0- - MarketDepthExchanges = def_message [80,0], - [ :request_id, :int ] + MarketDepthExchanges = def_message [80,0], + [ :request_id, :int ] TickRequestParameters = def_message [81, 0], [ :ticker_id, :int ], - [ :min_tick, :decimal], - [ :exchange, :string ], - [ :snapshot_permissions, :int ] -# class TickRequestParameters -# def load -# simple_load -# end + [ :min_tick, :decimal], + [ :exchange, :string ], + [ :snapshot_permissions, :int ] +# class TickRequestParameters +# def load +# simple_load +# end # end - ### Require standalone source files for more complex message classes: - require 'ib/messages/incoming/alert' - require 'ib/messages/incoming/contract_data' - require 'ib/messages/incoming/delta_neutral_validation' - require 'ib/messages/incoming/execution_data' - require 'ib/messages/incoming/historical_data' - require 'ib/messages/incoming/market_depths' - require 'ib/messages/incoming/next_valid_id' - require 'ib/messages/incoming/open_order' - require 'ib/messages/incoming/order_status' - require 'ib/messages/incoming/account_value' - require 'ib/messages/incoming/portfolio_value' - require 'ib/messages/incoming/real_time_bar' - require 'ib/messages/incoming/scanner_data' - require 'ib/messages/incoming/ticks' + RequestManagedAccounts = def_message 17 + AccountSummaryEnd = def_message 64 + + PositionDataEnd = def_message 62 + + PositionsMultiEnd = def_message 72 + + TickSnapshotEnd = def_message 57, [:ticker_id, :int] + + AccountUpdatesMultiEnd = def_message 74 + + AccountUpdateTime = def_message 8, [:time_stamp, :string] + + AccountValue = def_message([6, 2], AccountMessage, + [:account_value, :key, :symbol], + [:account_value, :value, :string], + [:account_value, :currency, :string], + [:account, :string]) + + + AccountUpdatesMulti = def_message( 73, + [ :request_id, :int ], + [ :account , :string ], + [ :model, :string ], + [ :key , :string ], + [ :value , :float], + [ :currency, :string ]) + AccountSummary = def_message(63, AccountMessage, + [:request_id, :int], + [ :account, :string ], + [:account_value, :key, :symbol], + [:account_value, :value, :string], + [:account_value, :currency, :string] + ) + + ### Require standalone source files for more complex message classes: +# require 'ib/messages/incoming/alert' +# require 'ib/messages/incoming/contract_data' +# require 'ib/messages/incoming/delta_neutral_validation' +# require 'ib/messages/incoming/execution_data' +# require 'ib/messages/incoming/historical_data' +# require 'ib/messages/incoming/market_depths' +# require 'ib/messages/incoming/next_valid_id' +# require 'ib/messages/incoming/open_order' +# require 'ib/messages/incoming/order_status' +# require 'ib/messages/incoming/account_value' +# require 'ib/messages/incoming/portfolio_value' +# require 'ib/messages/incoming/real_time_bar' +# require 'ib/messages/incoming/scanner_data' +# require 'ib/messages/incoming/ticks' +# end # module Incoming end # module Messages end # module IB @@ -208,7 +242,7 @@ class IN: TICK_SNAPSHOT_END = 57 MARKET_DATA_TYPE = 58 COMMISSION_REPORT = 59 ## - ### const below are new in api 9.71 + ### const below are new in api 9.71 POSITION_DATA = 61 POSITION_END = 62 ACCOUNT_SUMMARY = 63 @@ -248,4 +282,11 @@ class IN: HISTORICAL_TICKS_BID_ASK = 97 HISTORICAL_TICKS_LAST = 98 TICK_BY_TICK = 99 - + # VER 10 + ORDER_BOUND = 100 + COMPLETED_ORDER = 101 + COMPLETED_ORDERS_END = 102 + REPLACE_FA_END = 103 + WSH_META_DATA = 104 + WSH_EVENT_DATA = 105 + HISTORICAL_SCHEDULE = 106 diff --git a/lib/ib/messages/incoming/abstract_message.rb b/lib/ib/messages/incoming/abstract_message.rb index 7c1df0d..2febf5b 100644 --- a/lib/ib/messages/incoming/abstract_message.rb +++ b/lib/ib/messages/incoming/abstract_message.rb @@ -1,116 +1,116 @@ -require 'ib/messages/abstract_message' -require 'ib/support' +#require 'ib/messages/abstract_message' +#require 'ib/support' require 'ox' module IB - module Messages - module Incoming - using IBSupport # refine Array-method for decoding of IB-Messages + module Messages + module Incoming + using IB::Support # refine Array-method for decoding of IB-Messages - # Container for specific message classes, keyed by their message_ids - Classes = {} + # Container for specific message classes, keyed by their message_ids + Classes = {} - class AbstractMessage < IB::Messages::AbstractMessage + class AbstractMessage < IB::Messages::AbstractMessage - attr_accessor :buffer # is an array + attr_accessor :buffer # is an array - def version # Per message, received messages may have the different versions - @data[:version] - end + def version # Per message, received messages may have the different versions + @data[:version] + end - def check_version actual, expected - unless actual == expected || expected.is_a?(Array) && expected.include?(actual) - puts self.class.name - error "Unsupported version #{actual} received, expected #{expected}" - end - end + def check_version actual, expected + unless actual == expected || expected.is_a?(Array) && expected.include?(actual) + puts self.class.name + error "Unsupported version #{actual} received, expected #{expected}" + end + end - # Create incoming message from a given source (IB Socket or data Hash) - def initialize source - @created_at = Time.now - if source.is_a?(Hash) # Source is a @data Hash - @data = source - @buffer =[] # initialize empty buffer, indicates a successful initializing - else - @buffer = source - ### DEBUG DEBUG DEBUG RAW STREAM ############### - # if uncommented, the raw-input from the tws is included in the logging - ## puts "BUFFER :> #{buffer.inspect} " - ### DEBUG DEBUG DEBUG RAW STREAM ############### - @data = Hash.new - self.load - end - end + # Create incoming message from a given source (IB Socket or data Hash) + def initialize source + @created_at = Time.now + if source.is_a?(Hash) # Source is a @data Hash + @data = source + @buffer =[] # initialize empty buffer, indicates a successful initializing + else + @buffer = source + ### DEBUG DEBUG DEBUG RAW STREAM ############### + # if uncommented, the raw-input from the tws is included in the logging +## puts "BUFFER :> \n #{buffer.inspect} \n" + ### DEBUG DEBUG DEBUG RAW STREAM ############### + @data = Hash.new + self.load + end + end - def valid? - @buffer.empty? - end + def valid? + @buffer.empty? + end - ## more recent messages omit the transmission of a version - ## thus just load the parameter-map - def simple_load - load_map *self.class.data_map - rescue IB::Error => e - error "Reading #{self.class}: #{e.class}: #{e.message}", :load, e.backtrace - end - # Every message loads received message version first - # Override the load method in your subclass to do actual reading into @data. - def load - unless self.class.version.zero? - @data[:version] = buffer.read_int - check_version @data[:version], self.class.version - end - simple_load - end + ## more recent messages omit the transmission of a version + ## thus just load the parameter-map + def simple_load + load_map *self.class.data_map + rescue IB::Error => e + error "Reading #{self.class}: #{e.class}: #{e.message}", :load, e.backtrace + end + # Every message loads received message version first + # Override the load method in your subclass to do actual reading into @data. + def load + unless self.class.version.zero? + @data[:version] = buffer.read_int + check_version @data[:version], self.class.version + end + simple_load + end - # Load @data from the buffer according to the given data map. - # - # map is a series of Arrays in the format of - # [ :name, :type ], [ :group, :name, :type] - # type identifiers must have a corresponding read_type method on the buffer-class (read_int, etc.). - # group is used to lump together aggregates, such as Contract or Order fields - def load_map(*map) - map.each do |instruction| - # We determine the function of the first element - head = instruction.first - case head - when Integer # >= Version condition: [ min_version, [map]] - load_map *instruction.drop(1) if version >= head + # Load @data from the buffer according to the given data map. + # + # map is a series of Arrays in the format of + # [ :name, :type ], [ :group, :name, :type] + # type identifiers must have a corresponding read_type method on the buffer-class (read_int, etc.). + # group is used to lump together aggregates, such as Contract or Order fields + def load_map(*map) + map.each do |instruction| + # We determine the function of the first element + head = instruction.first + case head + when Integer # >= Version condition: [ min_version, [map]] + load_map *instruction.drop(1) if version >= head - when Proc # Callable condition: [ condition, [map]] - load_map *instruction.drop(1) if head.call + when Proc # Callable condition: [ condition, [map]] + load_map *instruction.drop(1) if head.call - when true # Pre-condition already succeeded! - load_map *instruction.drop(1) + when true # Pre-condition already succeeded! + load_map *instruction.drop(1) - when nil, false # Pre-condition already failed! Do nothing... + when nil, false # Pre-condition already failed! Do nothing... - when Symbol # Normal map - group, name, type, block = - if instruction[2].nil? || instruction[2].is_a?(Proc) # lambda's are Proc's - [nil] + instruction # No group, [ :name, :type, (:block) ] - else - instruction # [ :group, :name, :type, (:block)] - end - begin - data = @buffer.__send__("read_#{type}", &block) - rescue IB::LoadError, NoMethodError => e - error "Reading #{self.class}: #{e.class}: #{e.message} --> Instruction: #{name}" , :reader, false - end - # debug puts data.inspect - if group - @data[group] ||= {} - @data[group][name] = data - else - @data[name] = data - end - else - error "Unrecognized instruction #{instruction}" - end - end - end + when Symbol # Normal map + group, name, type, block = + if instruction[2].nil? || instruction[2].is_a?(Proc) # lambda's are Proc's + [nil] + instruction # No group, [ :name, :type, (:block) ] + else + instruction # [ :group, :name, :type, (:block)] + end + begin + data = @buffer.__send__("read_#{type}", &block) + rescue IB::LoadError, NoMethodError => e + error "Reading #{self.class}: #{e.class}: #{e.message} --> Instruction: #{name}" , :reader, false + end + # debug puts data.inspect + if group + @data[group] ||= {} + @data[group][name] = data + else + @data[name] = data + end + else + error "Unrecognized instruction #{instruction}" + end + end + end - end # class AbstractMessage - end # module Incoming - end # module Messages + end # class AbstractMessage + end # module Incoming + end # module Messages end # module IB diff --git a/lib/ib/messages/incoming/abstract_tick.rb b/lib/ib/messages/incoming/abstract_tick.rb new file mode 100644 index 0000000..72a7013 --- /dev/null +++ b/lib/ib/messages/incoming/abstract_tick.rb @@ -0,0 +1,25 @@ + +module IB + module Messages + module Incoming + extend Messages # def_message macros + class AbstractTick < AbstractMessage + # Returns Symbol with a meaningful name for received tick type + def type + TICK_TYPES[@data[:tick_type]] + end + + def to_human + "<#{self.message_type} #{type}:" + + @data.map do |key, value| + " #{key} #{value}" unless [:version, :ticker_id, :tick_type].include?(key) + end.compact.join('",') + " >" + end + + def the_data + @data.reject{|k,_| [:version, :ticker_id].include? k } + end + end + end + end +end diff --git a/lib/ib/messages/incoming/account_message.rb b/lib/ib/messages/incoming/account_message.rb new file mode 100644 index 0000000..9ade643 --- /dev/null +++ b/lib/ib/messages/incoming/account_message.rb @@ -0,0 +1,26 @@ +module IB + module Messages + module Incoming + + extend Messages # def_message macros + + + # Receives previously requested FA configuration information from TWS. + + class AccountMessage < AbstractMessage + def account_value + @account_value = IB::AccountValue.new @data[:account_value] + end + def account_name + @account_name = @data[:account] + end + + def to_human + "" - end - end - - # Receives previously requested FA configuration information from TWS. - ReceiveFA = - def_message 16, [:type, :int], # type of Financial Advisor configuration data - # being received from TWS. Valid values include: - # 1 = GROUPS, 2 = PROFILE, 3 = ACCOUNT ALIASES - [:xml, :xml] # XML string with requested FA configuration information. - - class ReceiveFA - def accounts - if( a= xml[:ListOfAccountAliases][:AccountAlias]).is_a? Array - a.map{|x| Account.new x } - elsif a.is_a? Hash ## only one account (soley financial advisor) - [ Account.new( a ) ] - end - end - - def to_human - "" - end - end - - - - class AccountMessage < AbstractMessage - def account_value - @account_value = IB::AccountValue.new @data[:account_value] - end - def account_name - @account_name = @data[:account] - end - - def to_human - " contract_detail) end @@ -56,14 +57,18 @@ def contract_detail @contract_detail = IB::ContractDetail.new @data[:contract_detail] end - def to_human - "" - end + def to_human + "" + end end # ContractData + + module ContractAccessors + + end BondContractData = - def_message [18, [4, 6]], ContractData, + def_message [18, [4, 6]], ContractDetails, [:request_id, :int], [:contract, :symbol, :string], [:contract, :sec_type, :string], diff --git a/lib/ib/messages/incoming/contract_message.rb b/lib/ib/messages/incoming/contract_message.rb new file mode 100644 index 0000000..5c74252 --- /dev/null +++ b/lib/ib/messages/incoming/contract_message.rb @@ -0,0 +1,13 @@ +module IB + module Messages + module Incoming + + # used by PortfolioValue + class ContractMessage < AbstractMessage + def contract + @contract = IB::Contract.build @data[:contract] + end + end + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/execution_data.rb b/lib/ib/messages/incoming/execution_data.rb index 7c0d287..96f800a 100644 --- a/lib/ib/messages/incoming/execution_data.rb +++ b/lib/ib/messages/incoming/execution_data.rb @@ -28,9 +28,9 @@ module Incoming class ExecutionData - def load - simple_load - end + def load + simple_load + end def contract @contract = IB::Contract.build @data[:contract] diff --git a/lib/ib/messages/incoming/histogram_data.rb b/lib/ib/messages/incoming/histogram_data.rb new file mode 100644 index 0000000..34aac06 --- /dev/null +++ b/lib/ib/messages/incoming/histogram_data.rb @@ -0,0 +1,30 @@ +module IB + module Messages + module Incoming + + HistogramData = def_message( [89,0], + [:request_id, :int], + [ :number_of_points , :int ]) do + # to human + "" + end + + class HistogramData + attr_accessor :results + using IB::Support # extended Array-Class from abstract_message + + def load + super + + @results = Array.new(@data[:number_of_points]) do |_| + { price: buffer.read_decimal, + count: buffer.read_int } + end + end + end + + + + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/historical_data.rb b/lib/ib/messages/incoming/historical_data.rb index 7d924a0..95f9fa9 100644 --- a/lib/ib/messages/incoming/historical_data.rb +++ b/lib/ib/messages/incoming/historical_data.rb @@ -3,16 +3,16 @@ module Messages module Incoming # HistoricalData contains following @data: - # + # # _General_: - # + # # - request_id - The ID of the request to which this is responding # - count - Number of Historical data points returned (size of :results). # - results - an Array of Historical Data Bars # - start_date - beginning of returned Historical data period # - end_date - end of returned Historical data period - # - # Each returned Bar in @data[:results] Array contains this data: + # + # Each returned Bar in @data[:results] Array contains this data: # - date - The date-time stamp of the start of the bar. The format is set to sec since EPOCHE # in outgoing/bar_requests ReqHistoricalData. # - open - The bar opening price. @@ -32,23 +32,23 @@ module Incoming [:count, :int] class HistoricalData attr_accessor :results - using IBSupport # extended Array-Class from abstract_message - + using IB::Support # extended Array-Class from abstract_message + def load super @results = Array.new(@data[:count]) do |_| IB::Bar.new :time => buffer.read_int_date, # conversion of epoche-time-integer to Dateime - # requires format_date in request to be "2" - # (outgoing/bar_requests # RequestHistoricalData#Encoding) + # requires format_date in request to be "2" + # (outgoing/bar_requests # RequestHistoricalData#Encoding) :open => buffer.read_float, :high => buffer.read_float, :low => buffer.read_float, :close => buffer.read_float, :volume => buffer.read_int, - :wap => buffer.read_float, + :wap => buffer.read_float, # :has_gaps => buffer.read_string, # only in ServerVersion < 124 - :trades => buffer.read_int + :trades => buffer.read_int end end @@ -58,45 +58,6 @@ def to_human end # HistoricalData - HistogramData = def_message( [89,0], - [:request_id, :int], - [ :number_of_points , :int ]) do - # to human - "" - end - - class HistogramData - attr_accessor :results - using IBSupport # extended Array-Class from abstract_message - - def load - super - - @results = Array.new(@data[:number_of_points]) do |_| - { price: buffer.read_decimal, - count: buffer.read_int } - end - end - end - - HistoricalDataUpdate = def_message [90, 0] , - [:request_id, :int] , - [:count, :int], - [:bar, :bar] # defined in support.rb - - class HistoricalDataUpdate - attr_accessor :results - using IBSupport # extended Array-Class from abstract_message - - def bar - @bar = IB::Bar.new @data[:bar] - end - - def to_human - "" - end - end - end # module Incoming diff --git a/lib/ib/messages/incoming/historical_data_update.rb b/lib/ib/messages/incoming/historical_data_update.rb new file mode 100644 index 0000000..c9354e2 --- /dev/null +++ b/lib/ib/messages/incoming/historical_data_update.rb @@ -0,0 +1,50 @@ + +module IB + module Messages + module Incoming + + HistoricalDataUpdate = def_message [90, 0] , + [:request_id, :int] , + [:count, :int], + [:bar, :bar] # defined in support.rb + + class HistoricalDataUpdate + attr_accessor :results + using IB::Support # extended Array-Class from abstract_message + + def bar + @bar = IB::Bar.new @data[:bar] + end + + def to_human + "" + end + end +#https://github.com/wizardofcrowds/ib-api/blob/3dd4851c838f61b2a6bbdc98a36b99499f90b701/lib/ib/messages/incoming/historical_data.rb HistoricalDataUpdate = def_message [90,0], +# [:request_id, :int], +# [:_, :int] +# # ["90", "2", "-1", "1612238280", "1.28285", "1.28275", "1.28285", "1.28275", "-1.0", "-1"] +# class HistoricalDataUpdate +# attr_accessor :results +# using IBSupport # extended Array-Class from abstract_message +# +# def load +# super +# # See Rust impl at https://github.com/sparkstartconsulting/IBKR-API-Rust/blob/d4e89c39a57a2b448bb912196ebc42acfb915be7/src/core/decoder.rs#L1097 +# @results = [ IB::Bar.new(:time => buffer.read_int_date, +# :open => buffer.read_decimal, +# :close => buffer.read_decimal, +# :high => buffer.read_decimal, +# :low => buffer.read_decimal, +# :wap => buffer.read_decimal, +# :volume => buffer.read_int) ] +# end +# +# def to_human +# "" +# end +#end # HistoricalDataUpdate + + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/managed_accounts.rb b/lib/ib/messages/incoming/managed_accounts.rb new file mode 100644 index 0000000..1e8b6c8 --- /dev/null +++ b/lib/ib/messages/incoming/managed_accounts.rb @@ -0,0 +1,21 @@ +module IB + module Messages + module Incoming + + + ManagedAccounts = + def_message 15, [:accounts_list, :string] + + class ManagedAccounts + def accounts + accounts_list.split(',').map{|a| Account.new account: a} + end + + def to_human + "" + end + end + + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/market_depths.rb b/lib/ib/messages/incoming/market_depth.rb similarity index 73% rename from lib/ib/messages/incoming/market_depths.rb rename to lib/ib/messages/incoming/market_depth.rb index 6b8cb9a..c99383a 100644 --- a/lib/ib/messages/incoming/market_depths.rb +++ b/lib/ib/messages/incoming/market_depth.rb @@ -28,16 +28,6 @@ def to_human end end - MarketDepthL2 = - def_message 13, MarketDepth, # Fields descriptions - see above - [:request_id, :int], - [:position, :int], - [:market_maker, :string], # The exchange hosting this order. - [:operation, :int], - [:side, :int], - [:price, :decimal], - [:size, :int] - end # module Incoming end # module Messages diff --git a/lib/ib/messages/incoming/market_depth_l2.rb b/lib/ib/messages/incoming/market_depth_l2.rb new file mode 100644 index 0000000..3b14c7c --- /dev/null +++ b/lib/ib/messages/incoming/market_depth_l2.rb @@ -0,0 +1,15 @@ +module IB + module Messages + module Incoming + MarketDepthL2 = + def_message 13, MarketDepth, # Fields descriptions - see above + [:request_id, :int], + [:position, :int], + [:market_maker, :string], # The exchange hosting this order. + [:operation, :int], + [:side, :int], + [:price, :decimal], + [:size, :int] + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/next_valid_id.rb b/lib/ib/messages/incoming/next_valid_id.rb index ea0ab7e..05e9f12 100644 --- a/lib/ib/messages/incoming/next_valid_id.rb +++ b/lib/ib/messages/incoming/next_valid_id.rb @@ -8,6 +8,7 @@ module Incoming NextValidID = NextValidId = def_message(9, [:local_id, :int]) class NextValidId + using IB::Support # Legacy accessor alias order_id local_id diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 9ada7e0..6f20d12 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -1,69 +1,70 @@ module IB module Messages module Incoming - using IBSupport + using IB::Support # OpenOrder is the longest message with complex processing logics OpenOrder = - def_message [5, 34], # updated to v. 34 according to python (decoder.py processOpenOrder) - [:order, :local_id, :int], - - [:contract, :contract], # read standard-contract - # [ con_id, symbol,. sec_type, expiry, strike, right, multiplier, - # exchange, currency, local_symbol, trading_class ] - - [:order, :action, :string], - [:order, :total_quantity, :decimal], - [:order, :order_type, :string], - [:order, :limit_price, :decimal], - [:order, :aux_price, :decimal], - [:order, :tif, :string], - [:order, :oca_group, :string], - [:order, :account, :string], - [:order, :open_close, :string], - [:order, :origin, :int], - [:order, :order_ref, :string], - [:order, :client_id, :int], - [:order, :perm_id, :int], - [:order, :outside_rth, :boolean], # (@socket.read_int == 1) - [:order, :hidden, :boolean], # (@socket.read_int == 1) - [:order, :discretionary_amount, :decimal], - [:order, :good_after_time, :string], - [:shares_allocation, :string], # deprecated! field - - [:order, :fa_group, :string], - [:order, :fa_method, :string], - [:order, :fa_percentage, :string], - [:order, :fa_profile, :string], - [:order, :model_code, :string], - [:order, :good_till_date, :string], - [:order, :rule_80a, :string], - [:order, :percent_offset, :decimal], - [:order, :settling_firm, :string], - [:order, :short_sale_slot, :int], - [:order, :designated_location, :string], - [:order, :exempt_code, :int], - [:order, :auction_strategy, :int], - [:order, :starting_price, :decimal], - [:order, :stock_ref_price, :decimal], - [:order, :delta, :decimal], - [:order, :stock_range_lower, :decimal], - [:order, :stock_range_upper, :decimal], - [:order, :display_size, :int], + def_message [5, 0], # updated to v. 34 according to python (decoder.py processOpenOrder) + [ :order, :local_id, :int], + + [ :contract, :contract], # read standard-contract + # [ con_id, symbol,. sec_type, expiry, strike, right, multiplier, + # exchange, currency, local_symbol, trading_class ] + + [ :order, :action, :string ], + [ :order, :total_quantity, :decimal ], + [ :order, :order_type, :string ], + [ :order, :limit_price, :decimal ], + [ :order, :aux_price, :decimal ], + [ :order, :tif, :string ], + [ :order, :oca_group, :string ], + [ :order, :account, :string ], + [ :order, :open_close, :string ], + [ :order, :origin, :int ], + [ :order, :order_ref, :string ], + [ :order, :client_id, :int ], + [ :order, :perm_id, :int ], + [ :order, :outside_rth, :boolean ], + [ :order, :hidden, :boolean ], + [ :order, :discretionary_amount, :decimal ], + [ :order, :good_after_time, :string ], + [ :shares_allocation, :string ], # skip_share_allocation + + [ :order, :fa_group, :string ], # fa_params + [ :order, :fa_method, :string ], # fa_params + [ :order, :fa_percentage, :string ], # fa_params + [ :order, :fa_profile, :string ], # fa_params + + [ :order, :model_code, :string ], + [ :order, :good_till_date, :string ], + [ :order, :rule_80a, :string ], + [ :order, :percent_offset, :decimal ], + [ :order, :settling_firm, :string ], + [ :order, :short_sale_slot, :int ], # short_sale_parameter + [ :order, :designated_location, :string ], # short_sale_parameter + [ :order, :exempt_code, :int ], # short_sale_parameter + [ :order, :auction_strategy, :int ], # auction_strategy + [ :order, :starting_price, :decimal ], # auction_strategy + [ :order, :stock_ref_price, :decimal ], # auction_strategy + [ :order, :delta, :decimal ], # auction_strategy + [ :order, :stock_range_lower, :decimal ], # auction_strategy + [ :order, :stock_range_upper, :decimal ], # auction_strategy + [ :order, :display_size, :int ], #@order.rth_only = @socket.read_boolean - [:order, :block_order, :boolean], - [:order, :sweep_to_fill, :boolean], - [:order, :all_or_none, :boolean], - [:order, :min_quantity, :int], - [:order, :oca_type, :int], - [:order, :etrade_only, :boolean], - [:order, :firm_quote_only, :boolean], - [:order, :nbbo_price_cap, :decimal], - [:order, :parent_id, :int], - [:order, :trigger_method, :int], - [:order, :volatility, :decimal], - [:order, :volatility_type, :int], - [:order, :delta_neutral_order_type, :string], - [:order, :delta_neutral_aux_price, :decimal] + [ :order, :block_order, :boolean ], + [ :order, :sweep_to_fill, :boolean ], + [ :order, :all_or_none, :boolean ], + [ :order, :min_quantity, :int ], + [ :order, :oca_type, :int ], + [ :order, :etrade_only, :boolean ], # skip etrade only + [ :order, :firm_quote_only, :boolean ], # skip firm quote only + [ :order, :nbbo_price_cap, :string ], # skip nbbo_price_cap + [ :order, :parent_id, :int ], + [ :order, :trigger_method, :int ], + [ :order, :volatility, :decimal ], # vol_order_params + [ :order, :volatility_type, :int ], # vol_order_params + [ :order, :delta_neutral_order_type,:string ], # vol_order_params + [ :order, :delta_neutral_aux_price, :decimal ] # vol_order_params class OpenOrder @@ -91,33 +92,31 @@ def status order.status end - def conditions - order.conditions - end + def conditions + order.conditions + end # Object accessors def order - @order ||= IB::Order.new @data[:order].merge(:order_state => order_state) + @order ||= IB::Order.new @data[ :order].merge(:order_state => order_state) end def order_state @order_state ||= IB::OrderState.new( - @data[:order_state].merge( - :local_id => @data[:order][:local_id], - :perm_id => @data[:order][:perm_id], - :parent_id => @data[:order][:parent_id], - :client_id => @data[:order][:client_id])) + @data[ :order_state].merge( + :local_id => @data[ :order ][ :local_id ], + :perm_id => @data[ :order ][ :perm_id ], + :parent_id => @data[ :order ][ :parent_id], + :client_id => @data[ :order ][ :client_id] ) ) end def contract - @contract ||= IB::Contract.build( - @data[:contract].merge(:underlying => underlying) - ) + @contract ||= IB::Contract.build( @data[ :contract].merge(:underlying => underlying)) end def underlying - @underlying = @data[:underlying_present] ? IB::Underlying.new(@data[:underlying]) : nil + @underlying = @data[ :underlying_present ] ? IB::Underlying.new(@data[ :underlying ] ) : nil end alias under_comp underlying @@ -125,141 +124,155 @@ def underlying def load super -# load_map [proc { | | (@data[:order][:delta_neutral_order_type] != 'None') }, - load_map [proc { | | filled?(@data[:order][:delta_neutral_order_type]) }, # todo Testcase! +# load_map [proc { | | (@data[ :order][:delta_neutral_order_type] != 'None') }, + load_map [ proc { | | filled?(@data[ :order][:delta_neutral_order_type ] ) }, # todo Testcase! # As of client v.52, we may receive delta... params in openOrder - [:order, :delta_neutral_con_id, :int], - [:order, :delta_neutral_settling_firm, :string], - [:order, :delta_neutral_clearing_account, :string], - [:order, :delta_neutral_clearing_intent, :string], - [:order, :delta_neutral_open_close, :string], - [:order, :delta_neutral_short_sale, :bool], - [:order, :delta_neutral_short_sale_slot, :int], - [:order, :delta_neutral_designated_location, :string] ], # end proc - [:order, :continuous_update, :int], - [:order, :reference_price_type, :int], - [:order, :trail_stop_price, :decimal], # not trail-orders. see below - [:order, :trailing_percent, :decimal], - [:order, :basis_points, :decimal], - [:order, :basis_points_type, :int], - - [:contract, :legs_description, :string], - - # As of client v.55, we receive in OpenOrder for Combos: - # Contract.orderComboLegs Array - # Order.leg_prices Array - [:contract, :combo_legs, :array, proc do |_| - IB::ComboLeg.new :con_id => @buffer.read_int, - :ratio => @buffer.read_int, - :action => @buffer.read_string, - :exchange => @buffer.read_string, - :open_close => @buffer.read_int, - :short_sale_slot => @buffer.read_int, - :designated_location => @buffer.read_string, - :exempt_code => @buffer.read_int - end], - [:order, :leg_prices, :array, proc { |_| buffer.read_decimal }], # needs testing - [:order, :combo_params, :hash ], - #, proc do |_| - # { tag: buffer.read_string, value: buffer.read_string } # needs testing - # end], - - [:order, :scale_init_level_size, :int], - [:order, :scale_subs_level_size, :int], - - [:order, :scale_price_increment, :decimal], - [proc { | | filled?(@data[:order][:scale_price_increment]) }, - # As of client v.54, we may receive scale order fields - [:order, :scale_price_adjust_value, :decimal], - [:order, :scale_price_adjust_interval, :int], - [:order, :scale_profit_offset, :decimal], - [:order, :scale_auto_reset, :boolean], - [:order, :scale_init_position, :int], - [:order, :scale_init_fill_qty, :decimal], - [:order, :scale_random_percent, :boolean] - ], - - [:order, :hedge_type, :string], - [proc { | | filled?(@data[:order][:hedge_type]) }, - # As of client v.49/50, we can receive hedgeType, hedgeParam - [:order, :hedge_param, :string] - ], - - [:order, :opt_out_smart_routing, :boolean], - [:order, :clearing_account, :string], - [:order, :clearing_intent, :string], - [:order, :not_held, :boolean], - - [:underlying_present, :boolean], - [proc { | | filled?(@data[:underlying_present]) }, - [:underlying, :con_id, :int], - [:underlying, :delta, :decimal], - [:underlying, :price, :decimal] - ], - - # TODO: Test Order with algo_params, scale and legs! - [:order, :algo_strategy, :string], - [proc { | | filled?(@data[:order][:algo_strategy]) }, - [:order, :algo_params, :hash] - ], - [:order, :solicided, :boolean], - [:order, :what_if, :boolean], - [:order_state, :status, :string], - # IB uses weird String with Java Double.MAX_VALUE to indicate no value here - [:order_state, :init_margin, :decimal], # :string], - [:order_state, :maint_margin, :decimal], # :string], - [:order_state, :equity_with_loan, :decimal], # :string], - [:order_state, :commission, :decimal], # May be nil! - [:order_state, :min_commission, :decimal], # May be nil! - [:order_state, :max_commission, :decimal], # May be nil! - [:order_state, :commission_currency, :string], - [:order_state, :warning_text, :string], - - - [:order, :random_size, :boolean], - [:order, :random_price, :boolean], - - ## todo: ordertype = PEG BENCH -- -> test! - [proc{ @data[:order][:order_type] == 'PEG BENCH' }, - [:order, :reference_contract_id, :int ], - [:order, :is_pegged_change_amount_decrease, :bool ], - [:order, :pegged_change_amount, :decimal ], - [:order, :reference_change_amount, :decimal ], - [:order, :reference_exchange_id, :string ] - ], - [:order , :conditions, :array, proc { IB::OrderCondition.make_from( @buffer ) } ], - [proc { !@data[:order][:conditions].blank? }, - [:order, :conditions_ignore_rth, :bool], - [:order, :conditions_cancel_order,:bool] - ], - [:order, :adjusted_order_type, :string], - [:order, :trigger_price, :decimal], - [:order, :trail_stop_price, :decimal], # cpp -source: Traillimit orders - [:order, :adjusted_stop_limit_price, :decimal], - [:order, :adjusted_trailing_amount, :decimal], - [:order, :adjustable_trailing_unit, :int], - - [:order, :soft_dollar_tier_name, :string_not_null], - [:order, :soft_dollar_tier_value, :string_not_null], - [:order, :soft_dollar_tier_display_name, :string_not_null], - [:order, :cash_qty, :decimal], - [:order, :mifid_2_decision_maker, :string_not_null ], ## correct appearance of fields below - [:order, :mifid_2_decision_algo, :string_not_null ], ## is not tested yet - [:order, :mifid_2_execution_maker, :string ], - [:order, :mifid_2_execution_algo, :string_not_null ], - [:order, :dont_use_auto_price_for_hedge, :string ], - [:order, :is_O_ms_container, :bool ], - [ :order, :discretionary_up_to_limit_price, :decimal ] - - + [ :order, :delta_neutral_con_id, :int ], + [ :order, :delta_neutral_settling_firm, :string ], + [ :order, :delta_neutral_clearing_account, :string ], + [ :order, :delta_neutral_clearing_intent, :string ], + [ :order, :delta_neutral_open_close, :string ], + [ :order, :delta_neutral_short_sale, :bool ], + [ :order, :delta_neutral_short_sale_slot, :int ], + [ :order, :delta_neutral_designated_location, :string ] ], # end proc + [ :order, :continuous_update, :int ], + [ :order, :reference_price_type, :int ], ### end VolOrderParams (Python) + [ :order, :trail_stop_price, :decimal ], # not trail-orders. see below + [ :order, :trailing_percent, :decimal ], + [ :order, :basis_points, :decimal ], + [ :order, :basis_points_type, :int ], + + [ :contract, :legs_description, :string ], + + # As of client v.55, we receive in OpenOrder for Combos: + # Contract.orderComboLegs Array + # Order.leg_prices Array + [ :contract, :combo_legs, :array, proc do |_| + IB::ComboLeg.new :con_id => @buffer.read_int, + :ratio => @buffer.read_int, + :action => @buffer.read_string, + :exchange => @buffer.read_string, + :open_close => @buffer.read_int, + :short_sale_slot => @buffer.read_int, + :designated_location => @buffer.read_string, + :exempt_code => @buffer.read_int + end ], + [ :order, :leg_prices, :array, proc { |_| buffer.read_decimal } ], # needs testing + [ :order, :combo_params, :hash ], + #, proc do |_| +# { tag: buffer.read_string, value: buffer.read_string } # needs testing +# end], + + [ :order, :scale_init_level_size, :int ], + [ :order, :scale_subs_level_size, :int ], + + [ :order, :scale_price_increment, :decimal ], + [ proc { | | filled?( @data[ :order ][ :scale_price_increment ] ) }, # true or false + [ :order, :scale_price_adjust_value, :decimal ], # if true + [ :order, :scale_price_adjust_interval, :int ] , + [ :order, :scale_profit_offset, :decimal ], + [ :order, :scale_auto_reset, :boolean ], + [ :order, :scale_init_position, :int ], + [ :order, :scale_init_fill_qty, :decimal ], + [ :order, :scale_random_percent, :boolean ] ], # end of scale price increment + + [ :order, :hedge_type, :string ], # can be nil + [ proc { | | filled?(@data[ :order ][ :hedge_type ] ) }, # true or false + [ :order, :hedge_param, :string ] ], # if true + + [ :order, :opt_out_smart_routing, :boolean ], + [ :order, :clearing_account, :string ], + [ :order, :clearing_intent, :string ], + [ :order, :not_held, :boolean ], + + [ :underlying_present, :boolean ], + [ proc { | | filled?(@data[ :underlying_present ] ) }, # true or false + [ :underlying, :con_id, :int ], + [ :underlying, :delta, :decimal ], + [ :underlying, :price, :decimal ] ], # end of underlying present? + + # TODO: Test Order with algo_params, scale and legs! + [ :order, :algo_strategy, :string], + [ proc { | | filled?(@data[ :order ][ :algo_strategy ] ) }, # true of false + [ :order, :algo_params, :hash ] ], # of true + + [ :order, :solicided, :boolean ], + +## whatif serverVersion >= MIN_SERVER_VER_WHAT_IF_EXT_FIELDS + [ :order, :what_if, :boolean ], + [ :order_state, :status, :string ], + [ :order_state, :init_margin_before, :decimal ], # nil unless what_if is true + [ :order_state, :maint_margin_before, :decimal ], # nil unless what_if is true + [ :order_state, :equity_with_loan_before, :decimal ], # nil unless what_if is true + [ :order_state, :init_margin_change, :decimal ], # nil unless what_if is true + [ :order_state, :maint_margin_change, :decimal ], # nil unless what_if is true + [ :order_state, :equity_with_loan_change, :decimal ], # nil unless what_if is true + [ :order_state, :init_margin_after, :decimal ], # nil unless what_if is true + [ :order_state, :maint_margin_after, :decimal ], # nil unless what_if is true + [ :order_state, :equity_with_loan_after, :decimal ], # nil unless what_if is true + [ :order_state, :commission, :decimal ], # nil unless what_if is true + [ :order_state, :min_commission, :decimal ], # nil unless what_if is true + [ :order_state, :max_commission, :decimal ], # nil unless what_if is true + [ :order_state, :commission_currency, :string ], # nil unless what_if is true + [ :order_state, :warning_text, :string ], # nil unless what_if is true + + + [ :order, :random_size, :boolean ], + [ :order, :random_price, :boolean ], + + ## todo: ordertype = PEG BENCH -- -> test! + [ proc { @data[ :order ][ :order_type ] == 'PEG BENCH' }, # true of false + [ :order, :reference_contract_id, :int ], + [ :order, :is_pegged_change_amount_decrease, :bool ], + [ :order, :pegged_change_amount, :decimal ], + [ :order, :reference_change_amount, :decimal ], + [ :order, :reference_exchange_id, :string ] ], # end special parameters PEG BENCH + + [ :order , :conditions, :array, proc { IB::OrderCondition.make_from( @buffer ) } ], + [ proc { !@data[ :order ][ :conditions ].blank? }, # true or false + [ :order, :conditions_ignore_rth, :bool ], + [ :order, :conditions_cancel_order,:bool ] ], + #AdjustedOrderParams + [ :order, :adjusted_order_type, :string ], + [ :order, :trigger_price, :decimal ], + [ :order, :trail_stop_price, :decimal ], # Traillimit orders + [ :order, :limit_price_offset, :decimal ], + [ :order, :adjusted_stop_price, :decimal ], + [ :order, :adjusted_stop_limit_price, :decimal ], + [ :order, :adjusted_trailing_amount, :decimal ], + [ :order, :adjustable_trailing_unit, :int ], + # SoftDollarTier + [ :order, :soft_dollar_tier_name, :string_not_null ], + [ :order, :soft_dollar_tier_value, :string_not_null ], + [ :order, :soft_dollar_tier_display_name, :string_not_null ], + [ :order, :cash_qty, :decimal ], + [ :order, :dont_use_auto_price_for_hedge, :bool ], + [ :order, :is_O_ms_container, :bool ], + [ :order, :discretionary_up_to_limit_price, :bool ], + [ :order, :use_price_management_algo, :bool ], + [ :order, :duration, :int ], + [ :order, :post_to_ats, :int ], + [ :order, :auto_cancel_parent, :bool ] + # not implemented now > Server Version 170 + # PEGBEST_PEGMID_OFFSETS: + # [:order, :min_trade_qty, :int ], + # [:order, :min_compete_size, :int ], + # [:order, :compete_against_best_offset, :decimal], + # [:order, :mid_offset_at_whole, :decimal ], + # [:order, :mid_offset_at_half, :decimal ] + # not implemented now > Server Version 183 + # [:order, :customer_account, :string] + # not implemented now > Server Version 184 + # [:order, :professional_customer, :bool] + end # Check if given value was set by TWS to something vaguely "positive" def filled? value -# puts "filled: #{value.class} --> #{value.to_s}" +# puts "filled: #{value.class} --> #{value.to_s}" case value when String - (!value.empty?)# && (value != :none) && (value !='None') + (!value.empty?)# && (value != :none) && (value !='None') when Float, Integer, BigDecimal value > 0 else diff --git a/lib/ib/messages/incoming/order_status.rb b/lib/ib/messages/incoming/order_status.rb index e06adb2..93f3882 100644 --- a/lib/ib/messages/incoming/order_status.rb +++ b/lib/ib/messages/incoming/order_status.rb @@ -44,7 +44,7 @@ module Incoming [:order_state, :last_fill_price, :decimal], [:order_state, :client_id, :int], [:order_state, :why_held, :string], - [:order_state, :market_cap_price, :decimal] + [:order_state, :market_cap_price, :decimal] class OrderStatus diff --git a/lib/ib/messages/incoming/portfolio_value.rb b/lib/ib/messages/incoming/portfolio_value.rb index e255402..1085719 100644 --- a/lib/ib/messages/incoming/portfolio_value.rb +++ b/lib/ib/messages/incoming/portfolio_value.rb @@ -2,74 +2,43 @@ module IB module Messages module Incoming - class ContractMessage < AbstractMessage - def contract - @contract = IB::Contract.build @data[:contract] - end - end - PortfolioValue = def_message [7, 8], ContractMessage, - [:contract, :contract], # read standard-contract - [:portfolio_value, :position, :decimal], - [:portfolio_value,:market_price, :decimal], - [:portfolio_value,:market_value, :decimal], - [:portfolio_value,:average_cost, :decimal], - [:portfolio_value,:unrealized_pnl, :decimal], # May be nil! - [:portfolio_value,:realized_pnl, :decimal], # May be nil! - [:account, :string] + PortfolioValue = def_message [7, 8], ContractMessage, + [:contract, :contract], # read standard-contract + [:portfolio_value, :position, :decimal], + [:portfolio_value,:market_price, :decimal], + [:portfolio_value,:market_value, :decimal], + [:portfolio_value,:average_cost, :decimal], + [:portfolio_value,:unrealized_pnl, :decimal], # May be nil! + [:portfolio_value,:realized_pnl, :decimal], # May be nil! + [:account, :string] - class PortfolioValue + class PortfolioValue - def to_human - # "" - portfolio_value.to_human - end - - def portfolio_value - unless @portfolio_value.present? - @portfolio_value = IB::PortfolioValue.new @data[:portfolio_value] - @portfolio_value.contract = contract - @portfolio_value.account = account - end - @portfolio_value # return_value - end + def to_human + # "" + portfolio_value.to_human + end + def portfolio_value + unless @portfolio_value.present? + @portfolio_value = IB::PortfolioValue.new @data[:portfolio_value] + @portfolio_value.contract = contract + @portfolio_value.account = account + end + @portfolio_value # return_value + end - def account_name - @account_name = @data[:account] - end + def account_name + @account_name = @data[:account] + end -# alias :to_human :portfolio_value - end # PortfolioValue +# alias :to_human :portfolio_value + end # PortfolioValue - PositionData = - def_message( [61,3] , ContractMessage, - [:account, :string], - [:contract, :contract], # read standard-contract -# [ con_id, symbol,. sec_type, expiry, strike, right, multiplier, - # primary_exchange, currency, local_symbol, trading_class ] - [:position, :decimal], # changed from int after Server Vers. MIN_SERVER_VER_FRACTIONAL_POSITIONS - [:price, :decimal] - ) do -# def to_human - " #{contract.to_human} ( Amount #{position}) : Market-Price #{price} >" - end - - PositionDataEnd = def_message( 62 ) - PositionsMulti = def_message( 71, ContractMessage, - [ :request_id, :int ], - [ :account, :string ], - [:contract, :contract], # read standard-contract - [ :position, :decimal], # changed from int after Server Vers. MIN_SERVER_VER_FRACTIONAL_POSITIONS - [ :average_cost, :decimal], - [ :model_code, :string ]) - - PositionsMultiEnd = def_message 72 - - diff --git a/lib/ib/messages/incoming/position_data.rb b/lib/ib/messages/incoming/position_data.rb new file mode 100644 index 0000000..73d62f2 --- /dev/null +++ b/lib/ib/messages/incoming/position_data.rb @@ -0,0 +1,21 @@ +module IB + module Messages + module Incoming + + PositionData = + def_message( [61,3] , ContractMessage, + [:account, :string], + [:contract, :contract], # read standard-contract +# [ con_id, symbol,. sec_type, expiry, strike, right, multiplier, + # primary_exchange, currency, local_symbol, trading_class ] + [:position, :decimal], # changed from int after Server Vers. MIN_SERVER_VER_FRACTIONAL_POSITIONS + [:price, :decimal] + ) do +# def to_human + " #{contract.to_human} ( Amount #{position}) : Market-Price #{price} >" + end + + + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/positions_multi.rb b/lib/ib/messages/incoming/positions_multi.rb new file mode 100644 index 0000000..7a0b4f1 --- /dev/null +++ b/lib/ib/messages/incoming/positions_multi.rb @@ -0,0 +1,15 @@ +module IB + module Messages + module Incoming + + + PositionsMulti = def_message( 71, ContractMessage, + [ :request_id, :int ], + [ :account, :string ], + [:contract, :contract], # read standard-contract + [ :position, :decimal], # changed from int after Server Vers. MIN_SERVER_VER_FRACTIONAL_POSITIONS + [ :average_cost, :decimal], + [ :model_code, :string ]) + end # module Incoming + end # module Messages +end # module IB diff --git a/lib/ib/messages/incoming/receive_fa.rb b/lib/ib/messages/incoming/receive_fa.rb new file mode 100644 index 0000000..8a26fad --- /dev/null +++ b/lib/ib/messages/incoming/receive_fa.rb @@ -0,0 +1,30 @@ +module IB + module Messages + module Incoming + + extend Messages # def_message macros + + + # Receives previously requested FA configuration information from TWS. + ReceiveFA = + def_message 16, [:type, :int], # type of Financial Advisor configuration data + # being received from TWS. Valid values include: + # 1 = GROUPS, 2 = PROFILE, 3 = ACCOUNT ALIASES + [:xml, :xml] # XML string with requested FA configuration information. + + class ReceiveFA + def accounts + if( a= xml[:ListOfAccountAliases][:AccountAlias]).is_a? Array + a.map{|x| Account.new x } + elsif a.is_a? Hash ## only one account (soley financial advisor) + [ Account.new( a ) ] + end + end + + def to_human + "" + end + end + end + end +end diff --git a/lib/ib/messages/incoming/scanner_data.rb b/lib/ib/messages/incoming/scanner_data.rb index e213263..b730787 100644 --- a/lib/ib/messages/incoming/scanner_data.rb +++ b/lib/ib/messages/incoming/scanner_data.rb @@ -18,7 +18,7 @@ module Incoming [:count, :int] class ScannerData attr_accessor :results - using IBSupport # extended Array-Class from abstract_message + using IB::Support # extended Array-Class from abstract_message def load super diff --git a/lib/ib/messages/incoming/tick_by_tick.rb b/lib/ib/messages/incoming/tick_by_tick.rb new file mode 100644 index 0000000..4e351d7 --- /dev/null +++ b/lib/ib/messages/incoming/tick_by_tick.rb @@ -0,0 +1,77 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + TickByTick = def_message [99, 0], [:ticker_id, :int ], + [ :tick_type, :int], + [ :time, :int_date ] + + ## error messages: (10189) "Failed to request tick-by-tick data:Historical data request pacing violation" + # + class TickByTick + using IB::Support # extended Array-Class from abstract_message + def resolve_mask + @data[:mask].present? ? [ @data[:mask] & 1 , @data[:mask] & 2 ] : [] + end + + def load + super + case @data[:tick_type ] + when 0 + # do nothing + when 1, 2 # Last, AllLast + load_map [ :price, :decimal ] , + [ :size, :int ] , + [ :mask, :int ] , + [ :exchange, :string ], + [ :special_conditions, :string ] + when 3 # bid/ask + load_map [ :bid_price, :decimal ], + [ :ask_price, :decimal], + [ :bid_size, :int ], + [ :ask_size, :int] , + [ :mask, :int ] + when 4 + load_map [ :mid_point, :decimal ] + end + + @out_labels = case @data[ :tick_tpye ] + when 1, 2 + [ "PastLimit", "Unreported" ] + when 3 + [ "BitPastLow", "BidPastHigh" ] + else + [] + end + end + def to_human + "< TickByTick:" + case @data[ :tick_type ] + when 1,2 + "(Last) #{size} @ #{price} [#{exchange}] " + when 3 + "(Bid/Ask) #{bid_size} @ #{bid_price} / #{ask_size } @ #{ask_price} " + when 4 + "(Midpoint) #{mid_point } " + else + "" + end + @out_labels.zip(resolve_mask).join( "/" ) + end + + [:price, :size, :mask, :exchange, :specialConditions, :bid_price, :ask_price, :bid_size, :ask_size, :mid_point].each do |name| + define_method name do + @data[name] + end + end + # def method_missing method, *args + # if @data.keys.include? method + # @data[method] + # else + # error "method #{method} not known" + # end + # end + end + end + end +end + diff --git a/lib/ib/messages/incoming/tick_efp.rb b/lib/ib/messages/incoming/tick_efp.rb new file mode 100644 index 0000000..7620c1b --- /dev/null +++ b/lib/ib/messages/incoming/tick_efp.rb @@ -0,0 +1,18 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + + TickEFP = def_message [47, 6], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:basis_points, :decimal], + [:formatted_basis_points, :string], + [:implied_futures_price, :decimal], + [:hold_days, :int], + [:dividend_impact, :decimal], + [:dividends_to_expiry, :decimal] + end + end +end diff --git a/lib/ib/messages/incoming/tick_generic.rb b/lib/ib/messages/incoming/tick_generic.rb new file mode 100644 index 0000000..cb84563 --- /dev/null +++ b/lib/ib/messages/incoming/tick_generic.rb @@ -0,0 +1,12 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + TickGeneric = def_message [45, 6], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:value, :float] + end + end +end diff --git a/lib/ib/messages/incoming/tick_option.rb b/lib/ib/messages/incoming/tick_option.rb new file mode 100644 index 0000000..f21e69c --- /dev/null +++ b/lib/ib/messages/incoming/tick_option.rb @@ -0,0 +1,60 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + # This message is received when the market in an option or its underlier moves. + # TWS option model volatilities, prices, and deltas, along with the present + # value of dividends expected on that options underlier are received. + # TickOption message contains following @data: + # :ticker_id - Id that was specified previously in the call to reqMktData() + # :tick_type - Specifies the type of option computation (see TICK_TYPES). + # :implied_volatility - The implied volatility calculated by the TWS option + # modeler, using the specified :tick_type value. + # :delta - The option delta value. + # :option_price - The option price. + # :pv_dividend - The present value of dividends expected on the options underlier + # :gamma - The option gamma value. + # :vega - The option vega value. + # :theta - The option theta value. + # :under_price - The price of the underlying. + TickOption = TickOptionComputation = + def_message([21, 0], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:tick_attribute, :int], + [:implied_volatility, :decimal_limit_1], # -1 and below + [:delta, :decimal_limit_2], # -2 and below + [:option_price, :decimal_limit_1], # -1 -"- + [:pv_dividend, :decimal_limit_1], # -1 -"- + [:gamma, :decimal_limit_2], # -2 -"- + [:vega, :decimal_limit_2], # -2 -"- + [:theta, :decimal_limit_2], # -2 -"- + [:under_price, :decimal_limit_1]) do + + "" + end + + class TickOption + def greeks + { delta: delta, gamma: gamma, vega: vega, theta: theta } + end + + def iv + implied_volatility + end + + + def greeks? + greeks.values.any? &:present? + end + + end + end + end +end diff --git a/lib/ib/messages/incoming/tick_price.rb b/lib/ib/messages/incoming/tick_price.rb new file mode 100644 index 0000000..adf2588 --- /dev/null +++ b/lib/ib/messages/incoming/tick_price.rb @@ -0,0 +1,60 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + # The IB code seems to dispatch up to two wrapped objects for this message, a tickPrice + # and sometimes a tickSize, which seems to be identical to the TICK_SIZE object. + # + # Important note from + # http://chuckcaplan.com/twsapi/index.php/void%20tickPrice%28%29 : + # + # "The low you get is NOT the low for the day as you'd expect it + # to be. It appears IB calculates the low based on all + # transactions after 4pm the previous day. The most inaccurate + # results occur when the stock moves up in the 4-6pm aftermarket + # on the previous day and then gaps open upward in the + # morning. The low you receive from TWS can be easily be several + # points different from the actual 9:30am-4pm low for the day in + # cases like this. If you require a correct traded low for the + # day, you can't get it from the TWS API. One possible source to + # help build the right data would be to compare against what Yahoo + # lists on finance.yahoo.com/q?s=ticker under the "Day's Range" + # statistics (be careful here, because Yahoo will use anti-Denial + # of Service techniques to hang your connection if you try to + # request too many bytes in a short period of time from them). For + # most purposes, a good enough approach would start by replacing + # the TWS low for the day with Yahoo's day low when you first + # start watching a stock ticker; let's call this time T. Then, + # update your internal low if the bid or ask tick you receive is + # lower than that for the remainder of the day. You should check + # against Yahoo again at time T+20min to handle the occasional + # case where the stock set a new low for the day in between + # T-20min (the real time your original quote was from, taking into + # account the delay) and time T. After that you should have a + # correct enough low for the rest of the day as long as you keep + # updating based on the bid/ask. It could still get slightly off + # in a case where a short transaction setting a new low appears in + # between ticks of data that TWS sends you. The high is probably + # distorted in the same way the low is, which would throw your + # results off if the stock traded after-hours and gapped down. It + # should be corrected in a similar way as described above if this + # is important to you." + # + # IB then emits at most 2 events on eWrapper: + # tickPrice( tickerId, tickType, price, canAutoExecute) + # tickSize( tickerId, sizeTickType, size) + TickPrice = def_message [1, 6], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:price, :decimal], + [:size, :int], + [:can_auto_execute, :int] + class TickPrice + def valid? + super && !price.zero? + end + end + end + end +end diff --git a/lib/ib/messages/incoming/tick_size.rb b/lib/ib/messages/incoming/tick_size.rb new file mode 100644 index 0000000..12b9a73 --- /dev/null +++ b/lib/ib/messages/incoming/tick_size.rb @@ -0,0 +1,55 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + # The IB code seems to dispatch up to two wrapped objects for this message, a tickPrice + # and sometimes a tickSize, which seems to be identical to the TICK_SIZE object. + # + # Important note from + # http://chuckcaplan.com/twsapi/index.php/void%20tickPrice%28%29 : + # + # "The low you get is NOT the low for the day as you'd expect it + # to be. It appears IB calculates the low based on all + # transactions after 4pm the previous day. The most inaccurate + # results occur when the stock moves up in the 4-6pm aftermarket + # on the previous day and then gaps open upward in the + # morning. The low you receive from TWS can be easily be several + # points different from the actual 9:30am-4pm low for the day in + # cases like this. If you require a correct traded low for the + # day, you can't get it from the TWS API. One possible source to + # help build the right data would be to compare against what Yahoo + # lists on finance.yahoo.com/q?s=ticker under the "Day's Range" + # statistics (be careful here, because Yahoo will use anti-Denial + # of Service techniques to hang your connection if you try to + # request too many bytes in a short period of time from them). For + # most purposes, a good enough approach would start by replacing + # the TWS low for the day with Yahoo's day low when you first + # start watching a stock ticker; let's call this time T. Then, + # update your internal low if the bid or ask tick you receive is + # lower than that for the remainder of the day. You should check + # against Yahoo again at time T+20min to handle the occasional + # case where the stock set a new low for the day in between + # T-20min (the real time your original quote was from, taking into + # account the delay) and time T. After that you should have a + # correct enough low for the rest of the day as long as you keep + # updating based on the bid/ask. It could still get slightly off + # in a case where a short transaction setting a new low appears in + # between ticks of data that TWS sends you. The high is probably + # distorted in the same way the low is, which would throw your + # results off if the stock traded after-hours and gapped down. It + # should be corrected in a similar way as described above if this + # is important to you." + # + # IB then emits at most 2 events on eWrapper: + # tickPrice( tickerId, tickType, price, canAutoExecute) + # tickSize( tickerId, sizeTickType, size) + + TickSize = def_message [2, 6], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:size, :int] + end + end +end + diff --git a/lib/ib/messages/incoming/tick_string.rb b/lib/ib/messages/incoming/tick_string.rb new file mode 100644 index 0000000..47c3b11 --- /dev/null +++ b/lib/ib/messages/incoming/tick_string.rb @@ -0,0 +1,13 @@ +module IB + module Messages + module Incoming + extend Messages # def_message macros + + TickString = def_message [46, 6], AbstractTick, + [:ticker_id, :int], + [:tick_type, :int], + [:value, :string] + end + end +end + diff --git a/lib/ib/messages/incoming/ticks.rb b/lib/ib/messages/incoming/ticks.rb deleted file mode 100644 index 21fb20e..0000000 --- a/lib/ib/messages/incoming/ticks.rb +++ /dev/null @@ -1,229 +0,0 @@ -# All message classes related to ticks located here -module IB - module Messages - module Incoming - - class AbstractTick < AbstractMessage - # Returns Symbol with a meaningful name for received tick type - def type - TICK_TYPES[@data[:tick_type]] - end - - def to_human - "<#{self.message_type} #{type}:" + - @data.map do |key, value| - " #{key} #{value}" unless [:version, :ticker_id, :tick_type].include?(key) - end.compact.join('",') + " >" - end - - def the_data - @data.reject{|k,_| [:version, :ticker_id].include? k } - end - end - - - # The IB code seems to dispatch up to two wrapped objects for this message, a tickPrice - # and sometimes a tickSize, which seems to be identical to the TICK_SIZE object. - # - # Important note from - # http://chuckcaplan.com/twsapi/index.php/void%20tickPrice%28%29 : - # - # "The low you get is NOT the low for the day as you'd expect it - # to be. It appears IB calculates the low based on all - # transactions after 4pm the previous day. The most inaccurate - # results occur when the stock moves up in the 4-6pm aftermarket - # on the previous day and then gaps open upward in the - # morning. The low you receive from TWS can be easily be several - # points different from the actual 9:30am-4pm low for the day in - # cases like this. If you require a correct traded low for the - # day, you can't get it from the TWS API. One possible source to - # help build the right data would be to compare against what Yahoo - # lists on finance.yahoo.com/q?s=ticker under the "Day's Range" - # statistics (be careful here, because Yahoo will use anti-Denial - # of Service techniques to hang your connection if you try to - # request too many bytes in a short period of time from them). For - # most purposes, a good enough approach would start by replacing - # the TWS low for the day with Yahoo's day low when you first - # start watching a stock ticker; let's call this time T. Then, - # update your internal low if the bid or ask tick you receive is - # lower than that for the remainder of the day. You should check - # against Yahoo again at time T+20min to handle the occasional - # case where the stock set a new low for the day in between - # T-20min (the real time your original quote was from, taking into - # account the delay) and time T. After that you should have a - # correct enough low for the rest of the day as long as you keep - # updating based on the bid/ask. It could still get slightly off - # in a case where a short transaction setting a new low appears in - # between ticks of data that TWS sends you. The high is probably - # distorted in the same way the low is, which would throw your - # results off if the stock traded after-hours and gapped down. It - # should be corrected in a similar way as described above if this - # is important to you." - # - # IB then emits at most 2 events on eWrapper: - # tickPrice( tickerId, tickType, price, canAutoExecute) - # tickSize( tickerId, sizeTickType, size) - TickPrice = def_message [1, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - [:price, :float], - [:size, :int], - [:can_auto_execute, :int] - class TickPrice - def valid? - super && !price.zero? - end - end - - TickSize = def_message [2, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - [:size, :int] - - TickGeneric = def_message [45, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - [:value, :float] - - TickString = def_message [46, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - [:value, :string] - - TickEFP = def_message [47, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - [:basis_points, :decimal], - [:formatted_basis_points, :string], - [:implied_futures_price, :decimal], - [:hold_days, :int], - [:dividend_impact, :decimal], - [:dividends_to_expiry, :decimal] - - # This message is received when the market in an option or its underlier moves. - # TWS option model volatilities, prices, and deltas, along with the present - # value of dividends expected on that options underlier are received. - # TickOption message contains following @data: - # :ticker_id - Id that was specified previously in the call to reqMktData() - # :tick_type - Specifies the type of option computation (see TICK_TYPES). - # :implied_volatility - The implied volatility calculated by the TWS option - # modeler, using the specified :tick_type value. - # :delta - The option delta value. - # :option_price - The option price. - # :pv_dividend - The present value of dividends expected on the options underlier - # :gamma - The option gamma value. - # :vega - The option vega value. - # :theta - The option theta value. - # :under_price - The price of the underlying. - TickOptionComputation = TickOption = - def_message([21, 6], AbstractTick, - [:ticker_id, :int], - [:tick_type, :int], - # What is the "not yet computed" indicator: - [:implied_volatility, :decimal_limit_1], # -1 and below - [:delta, :decimal_limit_2], # -2 and below - [:option_price, :decimal_limit_1], # -1 -"- - [:pv_dividend, :decimal_limit_1], # -1 -"- - [:gamma, :decimal_limit_2], # -2 -"- - [:vega, :decimal_limit_2], # -2 -"- - [:theta, :decimal_limit_2], # -2 -"- - [:under_price, :decimal_limit_1]) do - - "" - end - - class TickOption - def greeks - { delta: delta, gamma: gamma, vega: vega, theta: theta } - end - - def iv - implied_volatility - end - - - def greeks? - greeks.values.any? &:present? - end - - end - - TickSnapshotEnd = def_message 57, [:ticker_id, :int] - - TickByTick = def_message [99, 0], [:ticker_id, :int ], - [ :tick_type, :int], - [ :time, :int_date ] - - ## error messages: (10189) "Failed to request tick-by-tick data:Historical data request pacing violation" - # - class TickByTick - using IBSupport # extended Array-Class from abstract_message - - def resolve_mask - @data[:mask].present? ? [ @data[:mask] & 1 , @data[:mask] & 2 ] : [] - end - - def load - super - case @data[:tick_type ] - when 0 - # do nothing - when 1, 2 # Last, AllLast - load_map [ :price, :decimal ] , - [ :size, :int ] , - [ :mask, :int ] , - [ :exchange, :string ], - [ :special_conditions, :string ] - when 3 # bid/ask - load_map [ :bid_price, :decimal ], - [ :ask_price, :decimal], - [ :bid_size, :int ], - [ :ask_size, :int] , - [ :mask, :int ] - when 4 - load_map [ :mid_point, :decimal ] - end - - @out_labels = case @data[ :tick_tpye ] - when 1, 2 - [ "PastLimit", "Unreported" ] - when 3 - [ "BitPastLow", "BidPastHigh" ] - else - [] - end - end - def to_human - "< TickByTick:" + case @data[ :tick_type ] - when 1,2 - "(Last) #{size} @ #{price} [#{exchange}] " - when 3 - "(Bid/Ask) #{bid_size} @ #{bid_price} / #{ask_size } @ #{ask_price} " - when 4 - "(Midpoint) #{mid_point } " - else - "" - end + @out_labels.zip(resolve_mask).join( "/" ) - end - - [:price, :size, :mask, :exchange, :specialConditions, :bid_price, :ask_price, :bid_size, :ask_size, :mid_point].each do |name| - define_method name do - @data[name] - end - end - # def method_missing method, *args - # if @data.keys.include? method - # @data[method] - # else - # error "method #{method} not known" - # end - # end - end - end # module Incoming - end # module Messages -end # module IB diff --git a/lib/ib/messages/outgoing.rb b/lib/ib/messages/outgoing.rb index e125f8b..656c4a5 100644 --- a/lib/ib/messages/outgoing.rb +++ b/lib/ib/messages/outgoing.rb @@ -1,4 +1,4 @@ -require 'ib/messages/outgoing/abstract_message' +#require 'ib/messages/outgoing/abstract_message' # TODO: Don't instantiate messages, use their classes as just namespace for .encode/decode @@ -40,21 +40,21 @@ module Outgoing # subscription can have (for outgoing RequestScannerSubscription message). RequestScannerParameters = def_message 24 - RequestNewsArticle = def_message 84, - :request_id , # autogenerated - :provider_code, - :article_id, - :options # taglist + RequestNewsArticle = def_message 84, + :request_id , # autogenerated + :provider_code, + :article_id, + :options # taglist - RequestNewsProviders = def_message 85 # no further parameters - RequestHistoricalNews = def_message 86, - :request_id , # autogenerated - :con_id, - :provider_code, - :start, # date - :total_results, - :options # taglist + RequestNewsProviders = def_message 85 # no further parameters + RequestHistoricalNews = def_message 86, + :request_id , # autogenerated + :con_id, + :provider_code, + :start, # date + :total_results, + :options # taglist CancelNewsBulletins = def_message 13 @@ -63,7 +63,7 @@ module Outgoing ## Data format is: @data = { :id => ticker_id} CancelMarketData = def_message [2, 2] - CancelMarketDepth = def_message 11 + # CancelMarketDepth = def_message 11 ## moved to outgoing/request_market_depth.rb CancelScannerSubscription = def_message 23 CancelHistoricalData = def_message 25 CancelRealTimeBars = def_message 51 @@ -90,10 +90,11 @@ module Outgoing RequestFA = def_message 18, :fa_data_type # data = { :fa_data_type => int, :xml => String } ReplaceFA = def_message 19, :fa_data_type, :xml - # data = { :market_data_type => int } - # data => { :id => request_id (int), :contract => Contract } + ## RequestContractDetails + # parameters :id => request_id(int), (autogenerated if absent) + # :contract => Contract } # # Special case for options: "wildcards" in the Contract fields retrieve Option chains # strike = 0 means all strikes @@ -106,44 +107,43 @@ module Outgoing RequestContractDetails = RequestContractData = def_message([9, 8], :request_id , # autogenerated [:contract, :serialize_long, [:sec_id_type]]) - - # Requests security definition option parameters for viewing a contract's option chain - # request_id: The ID chosen for the request - # underlyingSymbol - # futFopExchange: The exchange on which the returned options are trading. - # Can be set to the empty string "" for all exchanges. - # underlyingSecType: The type of the underlying security, i.e. STK - # underlyingConId: the contract ID of the underlying security. - # con_id: + + # Requests security definition option parameters for viewing a contract's option chain + # request_id: The ID chosen for the request + # underlyingSymbol + # futFopExchange: The exchange on which the returned options are trading. + # Can be set to the empty string "" for all exchanges. + # underlyingSecType: The type of the underlying security, i.e. STK + # underlyingConId: the contract ID of the underlying security. + # con_id: # Responses via Messages::Incoming::SecurityDefinitionOptionParameter - RequestSecurityDefinitionOptionParameters = ReqSecDefOptParams = RequestOptionChainDefinition = def_message [78,0], - :request_id, # autogenerated if not specified - :symbol, # underlyingSymbol - [:exchange, ""], # futOptExchange - :sec_type, # underlyingSecType - :con_id # underlyingConId (required) - + RequestSecurityDefinitionOptionParameters = ReqSecDefOptParams = RequestOptionChainDefinition = def_message [78,0], + :request_id, # autogenerated if not specified + :symbol, # underlyingSymbol + [:exchange, ""], # futOptExchange + :sec_type, # underlyingSecType + :con_id # underlyingConId (required) # data = { :id => ticker_id (int), :contract => Contract, :num_rows => int } -# Requests venues for which market data is returned to updateMktDepthL2 - # returns MarketDepthExchanges-Message - # - RequestMarketDepthExchanges = # requires ServerVersion >= 112 - def_message 82 +# Requests venues for which market data is returned to updateMktDepthL2 + # returns MarketDepthExchanges-Message + # + RequestMarketDepthExchanges = # requires ServerVersion >= 112 + def_message 82 + - - ## actual Version supported is: 137 - ## changes: MIN_SERVER_VER_SMART_DEPTH: 146 --> insert 'is_smarth_depth' after 'num_rows' - ## then: 'is_smart_depth' (bool) has to be specified in CancelMarketDepth, too - # + ## actual Version supported is: 137 + ## changes: MIN_SERVER_VER_SMART_DEPTH: 146 --> insert 'is_smarth_depth' after 'num_rows' + ## then: 'is_smart_depth' (bool) has to be specified in CancelMarketDepth, too + # RequestMarketDepth = def_message([10, 5], - :request_id, # autogenerated if not specified + :request_id, # autogenerated if not specified [:contract, :serialize_supershort ], - :num_rows, - "") # mktDataOptionsStr. ## not supported by api + :num_rows, + "") # mktDataOptionsStr. ## not supported by api # When this message is sent, TWS responds with ExecutionData messages, each # containing the execution report that meets the specified criteria. @@ -158,7 +158,7 @@ module Outgoing # :exchange => Filter the results based on the order exchange # :side => Filter the results based on the order action: BUY/SELL/SSHORT RequestExecutions = def_message([7, 3], - :request_id, # autogenerated if not specified + :request_id, # autogenerated if not specified :client_id, :account, :time, # Format "yyyymmdd-hh:mm:ss" @@ -181,8 +181,8 @@ module Outgoing # exercised. Values are: # - 0 = do not override # - 1 = override - ExerciseOptions = def_message([ 21, 2 ], - # :request_id, # id -> required # todo : TEST + ExerciseOptions = def_message([ 21, 2 ], + # :request_id, # id -> required # todo : TEST [:contract, :serialize_short], :exercise_action, :exercise_quantity, @@ -197,7 +197,7 @@ module Outgoing # Then, before the opening of the next trading day, market data will automatically # switch back to real-time market data. # :market_data_type = 1 (:real_time) for real-time streaming, 2 (:frozen) for frozen market data - # = 3 (:delayed) for delayed streaming , 4 (:frozen_delayed) for frozen delayed + # = 3 (:delayed) for delayed streaming , 4 (:frozen_delayed) for frozen delayed RequestMarketDataType = def_message 59, [:market_data_type, lambda { |type| MARKET_DATA_TYPES.invert[type] || type }, []] @@ -211,54 +211,54 @@ module Outgoing # 'estimates' - Estimates # 'finstat' - Financial statements # 'snapshot' - Summary }a - # ReportsFinSummary Financial summary - #ReportsOwnership Company's ownership (Can be large in size) - #ReportSnapshot Company's financial overview - #ReportsFinStatements Financial Statements - #RESC Analyst Estimates - #CalendarReport Company's calendar + # ReportsFinSummary Financial summary + #ReportsOwnership Company's ownership (Can be large in size) + #ReportSnapshot Company's financial overview + #ReportsFinStatements Financial Statements + #RESC Analyst Estimates + #CalendarReport Company's calendar RequestFundamentalData = def_message([52,2], :request_id, # autogenerated if not specified - [:contract, :serialize, :primary_exchange], + [:contract, :serialize, :primary_exchange], :report_type, - "" ) + "" ) - # Returns the timestamp of earliest available historical data for a contract and data type. - # :what_to_show: type of data for head timestamp - "BID", "ASK", "TRADES", etc - # :use_rth : use regular trading hours only, 1 for yes or 0 for no - # format_data : set to 2 to obtain it like system time format in second ---> don't change - RequestHeadTimeStamp = - def_message( [87,0], :request_id, # autogenerated - [:contract, :serialize_short, [:primary_exchange,:include_expired] ], - [:use_rth, 1 ], - [:what_to_show, 'Trades' ], - [:format_date, 2 ] ) ## don't change! + # Returns the timestamp of earliest available historical data for a contract and data type. + # :what_to_show: type of data for head timestamp - "BID", "ASK", "TRADES", etc + # :use_rth : use regular trading hours only, 1 for yes or 0 for no + # format_data : set to 2 to obtain it like system time format in second ---> don't change + RequestHeadTimeStamp = + def_message( [87,0], :request_id, # autogenerated + [:contract, :serialize_short, [:primary_exchange,:include_expired] ], + [:use_rth, 1 ], + [:what_to_show, 'Trades' ], + [:format_date, 2 ] ) ## don't change! - CancelHeadTimeStamp = - def_message [90,0 ] # , :(request_)id #required + CancelHeadTimeStamp = + def_message [90,0 ] # , :(request_)id #required - RequestHistogramData = - def_message( [88, 0], :request_id, # autogenerated - [:contract, :serialize_short, [:primary_exchange,:include_expired] ], - [:use_rth, 1 ], - [:time_period ] ) + RequestHistogramData = + def_message( [88, 0], :request_id, # autogenerated + [:contract, :serialize_short, [:primary_exchange,:include_expired] ], + [:use_rth, 1 ], + [:time_period ] ) - CancelHistogramData = - def_message [89,0 ] # , :(request_)id required + CancelHistogramData = + def_message [89,0 ] # , :(request_)id required - ## Attention: If not reasonable data are used, simply nothing is returned. - ## There is no error message either. + ## Attention: If not reasonable data are used, simply nothing is returned. + ## There is no error message either. RequestCalculateImpliedVolatility = CalculateImpliedVolatility = RequestImpliedVolatility = def_message([ 54,3 ],:request_id, # autogenerated [:contract, :serialize_short], :option_price, :under_price, - [:implied_volatility_options_count, 0], - [:implied_volatility_options_conditions, '']) + [:implied_volatility_options_count, 0], + [:implied_volatility_options_conditions, '']) # data = { :request_id => int, :contract => Contract, # :volatility => double, :under_price => double } @@ -267,83 +267,48 @@ module Outgoing [:contract, :serialize_short], :volatility, :under_price, - [:implied_volatility_options_count, 0], - [:implied_volatility_options_conditions, '']) - - # Start receiving market scanner results through the ScannerData messages. - # @data = { :id => ticker_id (int), - # :number_of_rows => int: number of rows of data to return for a query. - # :instrument => The instrument type for the scan. Values include - # 'STK', - US stocks - # 'STOCK.HK' - Asian stocks - # 'STOCK.EU' - European stocks - # :location_code => Legal Values include: - # - STK.US - US stocks - # - STK.US.MAJOR - US stocks (without pink sheet) - # - STK.US.MINOR - US stocks (only pink sheet) - # - STK.HK.SEHK - Hong Kong stocks - # - STK.HK.ASX - Australian Stocks - # - STK.EU - European stocks - # :scan_code => The type of the scan, such as HIGH_OPT_VOLUME_PUT_CALL_RATIO. - # :above_price => double: Only contracts with a price above this value. - # :below_price => double: Only contracts with a price below this value. - # :above_volume => int: Only contracts with a volume above this value. - # :market_cap_above => double: Only contracts with a market cap above this - # :market_cap_below => double: Only contracts with a market cap below this value. - # :moody_rating_above => Only contracts with a Moody rating above this value. - # :moody_rating_below => Only contracts with a Moody rating below this value. - # :sp_rating_above => Only contracts with an S&P rating above this value. - # :sp_rating_below => Only contracts with an S&P rating below this value. - # :maturity_date_above => Only contracts with a maturity date later than this - # :maturity_date_below => Only contracts with a maturity date earlier than this - # :coupon_rate_above => double: Only contracts with a coupon rate above this - # :coupon_rate_below => double: Only contracts with a coupon rate below this - # :exclude_convertible => Exclude convertible bonds. - # :scanner_setting_pairs => Used with the scan_code to help further narrow your query. - # Scanner Setting Pairs are delimited by slashes, making - # this parameter open ended. Example is "Annual,true" - - # when used with 'Top Option Implied Vol % Gainers' scan - # would return annualized volatilities. - # :average_option_volume_above => int: Only contracts with average volume above this - # :stock_type_filter => Valid values are: - # 'ALL' (excludes nothing) - # 'STOCK' (excludes ETFs) - # 'ETF' (includes ETFs) } - # ------------ - # To learn all valid parameter values that a scanner subscription can have, - # first subscribe to ScannerParameters and send RequestScannerParameters message. - # Available scanner parameters values will be listed in received XML document. - RequestScannerSubscription = - def_message([22, 3], :request_id , - [:number_of_rows, -1], # was: EOL, - :instrument, - :location_code, - :scan_code, - :above_price, - :below_price, - :above_volume, - :market_cap_above, - :market_cap_below, - :moody_rating_above, - :moody_rating_below, - :sp_rating_above, - :sp_rating_below, - :maturity_date_above, - :maturity_date_below, - :coupon_rate_above, - :coupon_rate_below, - :exclude_convertible, - :average_option_volume_above, # ? - :scanner_setting_pairs, - :stock_type_filter) - - - - require 'ib/messages/outgoing/place_order' - require 'ib/messages/outgoing/bar_requests' - require 'ib/messages/outgoing/account_requests' - require 'ib/messages/outgoing/request_marketdata' - require 'ib/messages/outgoing/request_tick_data' + [:implied_volatility_options_count, 0], + [:implied_volatility_options_conditions, '']) + + + + RequestAccountUpdates = RequestAccountData = def_message([6, 2], + [:subscribe, true], + :account_code) + CancelAccountSummary = def_message 63 # :request_id required + # + # Note: The reqPositions function is not available in Introducing + # Broker or Financial Advisor master accounts that have very large + # numbers of subaccounts (> 50) to optimize the performance of TWS/IB + # Gateway v973+. Instead the function reqPositionsMulti can be used + # to subscribe to updates from individual subaccounts. Also not + # available with IBroker accounts configured for on-demand account + # lookup. + RequestPositions = def_message 61 + CancelPositions = def_message 64 + + # The function reqPositionsMulti can be used with any + # account structure to subscribe to positions updates for multiple + # accounts and/or models. The account and model parameters are + # optional if there are not multiple accounts or models available. + RequestPositionsMulti = def_message( 74, :request_id, # autogenerated + [ :account, 'ALL' ], + [:model_code, nil ] ) + + CancelPositionsMulti = def_message( 75, :request_id ) # required + + RequestAccountUpdatesMulti = def_message( 76, :request_id, # autogenerated + [ :account, 'ALL'], # account or account-group + [:model_code, nil], + [:leger_and_nlv, nil ]) + CancelAccountUpdatesMulti = def_message 77, :request_id # required + CancelMarketDepth = def_message([11, 1], :is_smart_depth) + # require 'ib/messages/outgoing/place_order' + # require 'ib/messages/outgoing/bar_requests' + # require 'ib/messages/outgoing/account_requests' + # require 'ib/messages/outgoing/request_marketdata' + # require 'ib/messages/outgoing/request_market_depth' + # require 'ib/messages/outgoing/request_tick_data' end # module Outgoing end # module Messages @@ -417,7 +382,7 @@ module Outgoing REQ_MKT_DEPTH_EXCHANGES = 82 REQ_SMART_COMPONENTS = 83 REQ_NEWS_ARTICLE = 84 in preparation - REQ_NEWS_PROVIDERS = 85 in preparatino + REQ_NEWS_PROVIDERS = 85 in preparatino REQ_HISTORICAL_NEWS = 86 in preparation REQ_HEAD_TIMESTAMP = 87 supported now @@ -436,4 +401,10 @@ module Outgoing REQ_HISTORICAL_TICKS = 96 REQ_TICK_BY_TICK_DATA = 97 CANCEL_TICK_BY_TICK_DATA = 98 - + # ver10 + REQ_COMPLETED_ORDERS = 99 + REQ_WSH_META_DATA = 100 + CANCEL_WSH_META_DATA = 101 + REQ_WSH_EVENT_DATA = 102 + CANCEL_WSH_EVENT_DATA = 103 + diff --git a/lib/ib/messages/outgoing/abstract_message.rb b/lib/ib/messages/outgoing/abstract_message.rb index 95a981f..51493f2 100644 --- a/lib/ib/messages/outgoing/abstract_message.rb +++ b/lib/ib/messages/outgoing/abstract_message.rb @@ -1,5 +1,3 @@ -require 'ib/messages/abstract_message' - module IB module Messages module Outgoing @@ -24,10 +22,7 @@ def initialize data={} # each one and postpending a '\0'. # def send_to socket - ### debugging of outgoing Messages - # puts "------sendto ---------(debugging output in outgoing/abstract_message)" - # puts socket.prepare_message( self.preprocess).inspect.split('\x00')[3..-1].inspect - # puts "------sendto ---------" + Connection.logger.debug to_s socket.send_messages self.preprocess #.each {|data| socket.write_data data} end @@ -39,7 +34,7 @@ def to_s # Pre-process encoded message Array before sending into socket, such as # changing booleans into 0/1 and stuff def preprocess - self.encode.flatten.map {|data| data == true ? 1 : data == false ? 0 : data } + self.encode.flatten.reject{ |x| x == "do not include"}.map {|data| data == true ? 1 : data == false ? 0 : data } end # Encode message content into (possibly, nested) Array of values. @@ -47,29 +42,30 @@ def preprocess # Most messages also contain (ticker, request or order) :id. # Then, content of @data Hash is encoded per instructions in data_map. # This method may be modified by message subclasses! - # - # If the version is zero, omit its apperance (for historical data) + # + # If the version is zero, omit its apperance (for redesigned message-types as place-order, historical-data, etc) def encode - ## create a proper request_id and erase :id and :ticker_id if nessesary - if self.class.properties?.include?(:request_id) - @data[:request_id] = if @data[:request_id].blank? && @data[:ticker_id].blank? && @data[:id].blank? - rand(9999) - else - @data[:id] || @data[:ticker_id] || @data[:request_id] - end - @data[:id] = @data[:ticker_id] = nil - end + ## create a proper request_id and erase :id and :ticker_id if nessesary + if self.class.properties?.include?(:request_id) + @data[:request_id] = if @data[:request_id].blank? && @data[:ticker_id].blank? && @data[:id].blank? + rand(9999) + else + @data[:id] || @data[:ticker_id] || @data[:request_id] + end + @data[:id] = @data[:ticker_id] = nil + end [ - self.class.version.zero? ? self.class.message_id : [ self.class.message_id, self.class.version ], + self.class.version.zero? ? self.class.message_id : [ self.class.message_id, self.class.version ], + # include :id, :ticker_id, :local_id or :order_id as first field of the message (if present) @data[:id] || @data[:ticker_id] ||# @data[:request_id] || # id, ticker_id, local_id, order_id - @data[:local_id] || @data[:order_id] || [], # do not appear in data_map + @data[:local_id] || @data[:order_id] || [], # do not appear in data_map self.class.data_map.map do |(field, default_method, args)| # but request_id does case when default_method.nil? @data[field] - + when default_method.is_a?(Symbol) # method name with args - @data[field].send default_method, *args + @data[field].send default_method, *args when default_method.respond_to?(:call) # callable with args default_method.call @data[field], *args diff --git a/lib/ib/messages/outgoing/bar_requests.rb b/lib/ib/messages/outgoing/bar_request_message.rb similarity index 95% rename from lib/ib/messages/outgoing/bar_requests.rb rename to lib/ib/messages/outgoing/bar_request_message.rb index 27c4128..78053a3 100644 --- a/lib/ib/messages/outgoing/bar_requests.rb +++ b/lib/ib/messages/outgoing/bar_request_message.rb @@ -1,6 +1,7 @@ module IB module Messages module Outgoing + extend Messages # def_message macros # Messages that request bar data have special processing of @data @@ -43,7 +44,7 @@ def parse data # # Version 3 RequestRealTimeBars = def_message [ 50, 3 ], BarRequestMessage, - :request_id # autogenerated if not specified + :request_id # autogenerated if not specified class RequestRealTimeBars def parse data @@ -62,17 +63,14 @@ def encode bar_size, data_type.to_s.upcase, @data[:use_rth] , - "XYZ" # not suported realtimebars option string - ] + "" # not suported realtimebars option string + ] end end # RequestRealTimeBars - ## python reference (9.74) - # def reqHistoricalData(self, reqId:TickerId , contract:Contract, endDateTime:str, - # durationStr:str, barSizeSetting:str, whatToShow:str, - # useRTH:int, formatDate:int, keepUpToDate:bool, chartOptions:TagValueList) + RequestHistoricalData = def_message [20, 0], BarRequestMessage, - :request_id # autogenerated if not specified + :request_id # autogenerated if not specified # - data = { # :contract => Contract: requested ticker description @@ -188,9 +186,9 @@ def encode data_type.to_s.upcase, 2 , # @data[:format_date], format-date is hard-coded as int_date in incoming/historicalData contract.serialize_legs , - @data[:keep_up_todate], # 0 / 1 - 'XYZ' # chartOptions:TagValueList - For internal use only. Use default value XYZ. - ] + @data[:keep_up_todate], # 0 / 1 + '' # chartOptions:TagValueList - For internal use only. Use default value XYZ. + ] end end # RequestHistoricalData diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 9b67c92..9d99799 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -1,209 +1,149 @@ module IB module Messages module Outgoing + extend Messages # def_message macros - # Data format is { :id => int: local_id, - # :contract => Contract, - # :order => Order } - PlaceOrder = def_message [3, 45] ## ServerVersion > 145: def_message[ 0,45 ] - ## server-version is not known at compilation time - ## Method call has to be replaced then - ## Max-Client_ver --> 144!! + PlaceOrder = def_message [ 3,0 ] class PlaceOrder - def encode -# server_version = Connection.current.server_version order = @data[:order] contract = @data[:contract] - error "contract has to be specified" unless contract.is_a? IB::Contract - [super[0..-1], -# [ [3,45, @data[:local_id] ], - contract.serialize_short(:primary_exchange, :sec_id_type), - - # main order fields - (order.side == :short ? 'SSHORT' : order.side == :short_exempt ? 'SSHORTX' : order.side.to_sup), - order.total_quantity, - order[:order_type], # Internal code, 'LMT' instead of :limit - order.limit_price, - order.aux_price, - order[:tif], - order.oca_group, - order.account, - order.open_close.to_sup[0], - order[:origin], # translates :customer, :firm to 0,1 - order.order_ref, - order.transmit, - order.parent_id, - order.block_order || false, - order.sweep_to_fill || false, - order.display_size, - order[:trigger_method], - order.outside_rth || false, # was: ignore_rth - order.hidden || false, - contract.serialize_legs(:extended), - - - if contract.bag? - [ - ## Support for per-leg prices in Order - [contract.combo_legs.size] + contract.combo_legs.map { |_| nil }, #(&:price) , - ## Support for combo routing params in Order - order.combo_params.empty? ? 0 : [order.combo_params.size] + order.combo_params.to_a - ] - else - [] - end, - - - "", # deprecated shares_allocation field - order.discretionary_amount, - order.good_after_time, - order.good_till_date, - [ order.fa_group, - order.fa_method, - order.fa_percentage, - order.fa_profile ] , - order.model_code || "", - order[:short_sale_slot] || 0 , # 0 only for retail, 1 or 2 for institution (Institutional) - order.designated_location, # only populate when short_sale_slot == 2 (Institutional) - order.exempt_code, - order[:oca_type], - order[:rule_80a], #.to_sup[0..0], - order.settling_firm, - order.all_or_none || false, - order.min_quantity || "", - order.percent_offset || '', - false, # was: order.etrade_only || false, desupported in TWS > 981 - false, # was: order.firm_quote_only || false, desupported in TWS > 981 - order.nbbo_price_cap || "", ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here - order[:auction_strategy], - order.starting_price, - order.stock_ref_price || "", - order.delta || "", - order.stock_range_lower || "", - order.stock_range_upper || "", - order.override_percentage_constraints || false, - if order.volatility.present? - [ order.volatility , # Volatility orders - order[:volatility_type] || 2 ] # default: annual volatility - else - ["",""] - end, - # Support for delta neutral orders with parameters - if order.delta_neutral_order_type && order.delta_neutral_order_type != :none - [order[:delta_neutral_order_type], - order.delta_neutral_aux_price || "", - order.delta_neutral_con_id, - order.delta_neutral_settling_firm, - order.delta_neutral_clearing_account, - order[:delta_neutral_clearing_intent], - order.delta_neutral_open_close, - order.delta_neutral_short_sale, - order.delta_neutral_short_sale_slot, - order.delta_neutral_designated_location ] - else - ['', ''] - end, - - order.continuous_update, # Volatility orders - order[:reference_price_type] || "", # Volatility orders - - order.trail_stop_price || "", # TRAIL_STOP_LIMIT stop price - order.trailing_percent || "", # Support for trailing percent - - order.scale_init_level_size || "", # Scale Orders - order.scale_subs_level_size || "", # Scale Orders - order.scale_price_increment || "", # Scale Orders - - # Support for extended scale orders parameters - if order.scale_price_increment && order.scale_price_increment > 0 - [order.scale_price_adjust_value || "", - order.scale_price_adjust_interval || "", - order.scale_profit_offset || "", - order.scale_auto_reset, # default: false, - order.scale_init_position || "", - order.scale_init_fill_qty || "", - order.scale_random_percent # default: false, - ] - else - [] - end, - - order.scale_table, # v 69 - order.active_start_time || "" , # v 69 - order.active_stop_time || "" , # v 69 - - # Support for hedgeType - order.hedge_type, # MIN_SERVER_VER_HEDGE_ORDERS - order.hedge_param || [], - - order.opt_out_smart_routing, # MIN_SERVER_VER_OPT_OUT_SMART_ROUTING - - order.clearing_account , - order.clearing_intent , - order.not_held , - contract.serialize_under_comp, - order.serialize_algo(), - order.what_if, - order.serialize_misc_options, # MIN_SERVER_VER_LINKING - order.solicided , # MIN_SERVER_VER_ORDER_SOLICITED - order.random_size , # MIN_SERVER_VER_RANDOMIZE_SIZE_AND_PRICE - order.random_price , # MIN_SERVER_VER_RANDOMIZE_SIZE_AND_PRICE - ( order[:type] == 'PEG BENCH' ? [ # pegged_to_benchmark v. 102 - order.reference_contract_id, - order.is_pegged_change_amount_decrease, - order.pegged_change_amount, - order.reference_change_amount, - order.reference_exchange_id ] : [] ), - order.serialize_conditions , # serialisation of conditions outsourced to model file - order.adjusted_order_type , - order.trigger_price , - order.limit_price_offset , - order.adjusted_stop_price , - order.adjusted_stop_limit_price , - order.adjusted_trailing_amount , - order.adjustable_trailing_unit , - order.ext_operator , # MIN_SERVER_VER_EXT_OPERATOR: - order.soft_dollar_tier_name, - order.soft_dollar_tier_value, - order.soft_dollar_tier_display_name, -# order.serialize_soft_dollar_tier() , # MIN_SERVER_VER_SOFT_DOLLAR_TIER - order.cash_qty , # MIN_SERVER_VER_CASH_QTY /111) -# if server_version >= 138 # :min_server_ver_decision_maker - [ order.mifid_2_decision_maker, order.mifid_2_decision_algo], -# end , -# if server_version >= 139 # min_server_ver_mifid_execution - [ order.mifid_2_execution_maker, order.mifid_2_execution_algo ], -# end, -# if server_version >= 141 # min_server_ver_auto_price_for_hedge - order.dont_use_auto_price_for_hedge, -# end, -# if server_version >= 145 # min_server_ver_order_container - order.is_O_ms_container, -# end, -# if server_version >= 148 # min_server_ver_d_peg_orders - order.discretionary_up_to_limit_price -# end ] - ] -# -# -# -# if self.serverVersion() >= MIN_SERVER_VER_AUTO_PRICE_FOR_HEDGE:141 -# flds.append(make_field(order.dontUseAutoPriceForHedge)) -# -# if self.serverVersion() >= MIN_SERVER_VER_ORDER_CONTAINER:145 -# flds.append(make_field(order.isOmsContainer)) -# -# if self.serverVersion() >= MIN_SERVER_VER_D_PEG_ORDERS: 148 -# flds.append(make_field(order.discretionaryUpToLimitPrice)) -# -# + contract = order.contract unless contract.is_a? IB::Contract + + error 'contract has to be specified' unless contract.is_a? IB::Contract + + # send place order msg + fields = [ super , + contract.serialize_short(:primary_exchange, :sec_id_type), + order.serialize_main_order_fields, + order.serialize_extended_order_fields, + order.serialize_combo_legs(contract), + order.serialize_auxilery_order_fields # including advisory order fields + ] + +# if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] # 103 + fields.push(order.model_code ) + # end + + fields += [ + order[:short_sale_slot] , # 0 only for retail, 1 or 2 for institution (Institutional) + order.designated_location # only populate when short_sale_slot == 2 (Institutional) + ] + + fields.push(order.exempt_code) #if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] + + fields.push(order[:oca_type]) + fields += [ + order[:rule_80a], # .to_sup[0..0], + order.settling_firm, + order.all_or_none, + order.min_quantity, + order.percent_offset, + false, # etrade_only , desupported in TWS > 981 + false, # firm_quote_only , desupported in TWS > 981 + '', ## desupported in TWS > 981, too. + order[:auction_strategy], # one of: AUCTION_MATCH, AUCTION_IMPROVEMENT, AUCTION_TRANSPARENT + order.serialize_advanced_option_order_fields, + order.override_percentage_constraints, + order.serialize_volatility_order_fields, + order.serialize_delta_neutral_order_fields + ] + + fields += [ + order.continuous_update, + order[:reference_price_type] , + order.trail_stop_price, + order.trailing_percent + ] + + fields << order.serialize_scale_order_fields + + fields.push order.hedge_type + fields.push order.hedge_param # default is [] --> omitted if left default + fields.push order.opt_out_smart_routing + + fields.push order.clearing_account + fields.push order.clearing_intent + + fields.push(order.not_held) + + fields << contract.serialize_under_comp + fields << order.serialize_algo + + fields.push(order.algo_id) + fields.push(order.what_if) + fields.push(order.serialize_misc_options) + fields.push(order.solicided) + fields << [ order.random_size, order.random_price ] + + fields << order.serialize_pegged_order_fields + fields << order.serialize_conditions + fields << [ + order.adjusted_order_type, + order.trigger_price, + order.limit_price_offset, + order.adjusted_stop_price, + order.adjusted_stop_limit_price, + order.adjusted_trailing_amount, + order.adjustable_trailing_unit + ] + + fields.push(order.ext_operator) # if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] # 105 + + fields << order.serialize_soft_dollar_tier + + fields.push(order.cash_qty) # if server_version >= KNOWN_SERVERS[:min_server_ver_cash_qty] # 111 + + fields << order.serialize_mifid_order_fields + +# if server_version >= KNOWN_SERVERS[:min_server_ver_auto_price_for_hedge] # 141 + fields.push(order.dont_use_auto_price_for_hedge) +# end + + fields.push(order.is_O_ms_container) #if server_version >= KNOWN_SERVERS[:min_server_ver_order_container] # 145 + + if server_version >= KNOWN_SERVERS[:min_server_ver_d_peg_orders] # 148 + fields.push(order.discretionary_up_to_limit_price) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_price_mgmt_algo] # 151 + fields.push(order.use_price_management_algo) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_duration] # 158 + fields.push(order.duration) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_post_to_ats] # 160 + fields.push(order.post_to_ats) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_auto_cancel_parent] # 162 + fields.push(order.auto_cancel_parent) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_advanced_order_reject] # 166 + fields.push(order.advanced_order_reject) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_manual_order_time] # 169 + fields.push(order.manual_order_time) + end + + fields << order.serialize_peg_best_and_mid + + if server_version >= KNOWN_SERVERS[:min_server_ver_customer_account] # 183 + fields.push(order.customer_account) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_professional_customer] # 184 + fields.push(order.professional_account) + end + fields end - end # PlaceOrder - - - end # module Outgoing - end # module Messages -end # module IB + end + end + end +end diff --git a/lib/ib/messages/outgoing/account_requests.rb b/lib/ib/messages/outgoing/request_account_summary.rb similarity index 60% rename from lib/ib/messages/outgoing/account_requests.rb rename to lib/ib/messages/outgoing/request_account_summary.rb index cc637a7..37b59b9 100644 --- a/lib/ib/messages/outgoing/account_requests.rb +++ b/lib/ib/messages/outgoing/request_account_summary.rb @@ -1,15 +1,8 @@ - module IB module Messages module Outgoing + extend Messages # def_message macros - RequestManagedAccounts = def_message 17 - - # @data = { :subscribe => boolean, - # :account_code => Advisor accounts only. Empty ('') for a standard account. } - RequestAccountUpdates = RequestAccountData = def_message([6, 2], - [:subscribe, true], - :account_code) =begin Call this method to request and keep up to date the data that appears on the TWS Account Window Summary tab. The data is returned by @@ -66,41 +59,15 @@ module Outgoing currencies. =end - RequestAccountSummary = def_message( 62, - :request_id, # autogenerated if not specified - [:group, 'All'], - :tags ) - - CancelAccountSummary = def_message 63 # :request_id required - - # Note: The reqPositions function is not available in Introducing - # Broker or Financial Advisor master accounts that have very large - # numbers of subaccounts (> 50) to optimize the performance of TWS/IB - # Gateway v973+. Instead the function reqPositionsMulti can be used - # to subscribe to updates from individual subaccounts. Also not - # available with IBroker accounts configured for on-demand account - # lookup. - RequestPositions = def_message 61 - CancelPositions = def_message 64 + RequestAccountSummary = def_message( 62, + :request_id, # autogenerated if not specified + [:group, 'All'], + :tags ) - - # The function reqPositionsMulti can be used with any - # account structure to subscribe to positions updates for multiple - # accounts and/or models. The account and model parameters are - # optional if there are not multiple accounts or models available. - RequestPositionsMulti = def_message( 74, :request_id, # autogenerated - [ :account, 'ALL' ], - [:model_code, nil ] ) - CancelPositionsMulti = def_message( 75, :request_id ) # required - RequestAccountUpdatesMulti = def_message( 76, :request_id, # autogenerated - [ :account, 'ALL'], # account or account-group - [:model_code, nil], - [:leger_and_nlv, nil ]) - CancelAccountUpdatesMulti = def_message 77, :request_id # required - end # module outgoing - end # module messages + end # module outgoing + end # module messages end # module ib # REQ_POSITIONS = 61 diff --git a/lib/ib/messages/outgoing/request_historical_data.rb b/lib/ib/messages/outgoing/request_historical_data.rb new file mode 100644 index 0000000..7346f71 --- /dev/null +++ b/lib/ib/messages/outgoing/request_historical_data.rb @@ -0,0 +1,182 @@ +module IB + module Messages + module Outgoing + extend Messages # def_message macros + + + RequestHistoricalData = def_message [20, 0], BarRequestMessage, + :request_id # autogenerated if not specified + + # - data = { + # :contract => Contract: requested ticker description + # :end_date_time => String: "yyyymmdd HH:mm:ss", with optional time zone + # allowed after a space: "20050701 18:26:44 GMT" + # :duration => String, time span the request will cover, and is specified + # using the format: , eg: '1 D', valid units are: + # '1 S' (seconds, default if no unit is specified) + # '1 D' (days) + # '1 W' (weeks) + # '1 M' (months) + # '1 Y' (years, currently limited to one) + # :bar_size => String: Specifies the size of the bars that will be returned + # (within IB/TWS limits). Valid values include: + # '1 sec' + # '5 secs' + # '15 secs' + # '30 secs' + # '1 min' + # '2 mins' + # '3 mins' + # '5 mins' + # '15 mins' + # '30 min' + # '1 hour' + # '1 day' + # :what_to_show => Symbol: Determines the nature of data being extracted. + # Valid values: + # :trades, :midpoint, :bid, :ask, :bid_ask, + # :historical_volatility, :option_implied_volatility, + # :option_volume, :option_open_interest + # - converts to "TRADES," "MIDPOINT," "BID," etc... + # :use_rth => int: 0 - all data available during the time span requested + # is returned, even data bars covering time intervals where the + # market in question was illiquid. 1 - only data within the + # "Regular Trading Hours" of the product in question is returned, + # even if the time span requested falls partially or completely + # outside of them. + # :format_date => int: 1 - text format, like "20050307 11:32:16". + # 2 - offset from 1970-01-01 in sec (UNIX epoch) + # } + # + # - NB: using the D :duration only returns bars in whole days, so requesting "1 D" + # for contract ending at 08:05 will only return 1 bar, for 08:00 on that day. + # But requesting "86400 S" gives 86400/barlengthsecs bars before the end Time. + # + # - Note also that the :duration for any request must be such that the start Time is not + # more than one year before the CURRENT-Time-less-one-day (not 1 year before the end + # Time in the Request) + # + # Bar Size Max Duration + # -------- ------------ + # 1 sec 2000 S + # 5 sec 10000 S + # 15 sec 30000 S + # 30 sec 86400 S + # 1 minute 86400 S, 6 D + # 2 minutes 86400 S, 6 D + # 5 minutes 86400 S, 6 D + # 15 minutes 86400 S, 6 D, 20 D, 2 W + # 30 minutes 86400 S, 34 D, 4 W, 1 M + # 1 hour 86400 S, 34 D, 4 w, 1 M + # 1 day 60 D, 12 M, 52 W, 1 Y + # + # - NB: as of 4/07 there is no historical data available for forex spot. + # + # - data[:contract] may either be a Contract object or a String. A String should be + # in serialize_ib_ruby format; that is, it should be a colon-delimited string in + # the format (e.g. for Globex British pound futures contract expiring in Sep-2008): + # + # + # - Fields not needed for a particular security should be left blank (e.g. strike + # and right are only relevant for options.) + # + # - A Contract object will be automatically serialized into the required format. + # + # - See also http://chuckcaplan.com/twsapi/index.php/void%20reqIntradayData%28%29 + # for general information about how TWS handles historic data requests, whence + # the following has been adapted: + # + # - The server providing historical prices appears to not always be + # available outside of market hours. If you call it outside of its + # supported time period, or if there is otherwise a problem with + # it, you will receive error #162 "Historical Market Data Service + # query failed.:HMDS query returned no data." + # + # - For backfill on futures data, you may need to leave the Primary + # Exchange field of the Contract structure blank; see + # http://www.interactivebrokers.com/discus/messages/2/28477.html?1114646754 + # + # - Version 6 implemented --> the version is not transmitted anymore + class RequestHistoricalData + def parse data + data_type, bar_size, contract = super data + + size = data[:bar_size] || data[:size] + bar_size = BAR_SIZES.invert[size] || size + unless BAR_SIZES.keys.include?(bar_size) + error ":bar_size must be one of #{BAR_SIZES.inspect}", :args + end + [data_type, bar_size, contract] + end + + def encode + data_type, bar_size, contract = parse @data + + [super.flatten, + contract.serialize_long[0..-1], # omit sec_id_type and sec_id + @data[:end_date_time], + bar_size, + @data[:duration], + @data[:use_rth], + data_type.to_s.upcase, + 2 , # @data[:format_date], format-date is hard-coded as int_date in incoming/historicalData + contract.serialize_legs , + @data[:keep_up_todate], # 0 / 1 + '' # chartOptions:TagValueList - For internal use only. Use default value XYZ. + ] + end + end # RequestHistoricalData + + end # module Outgoing + end # module Messages +end # module IB + +## python documentaion +# """Requests contracts' historical data. When requesting historical data, a +# finishing time and date is required along with a duration string. The +# resulting bars will be returned in EWrapper.historicalData() +# reqId:TickerId - The id of the request. Must be a unique value. When the +# market data returns, it whatToShowill be identified by this tag. This is also +# used when canceling the market data. +# contract:Contract - This object contains a description of the contract for which +# market data is being requested. +# endDateTime:str - Defines a query end date and time at any point during the past 6 mos. +# Valid values include any date/time within the past six months in the format: +# yyyymmdd HH:mm:ss ttt +# where "ttt" is the optional time zone. +# durationStr:str - Set the query duration up to one week, using a time unit +# of seconds, days or weeks. Valid values include any integer followed by a space +# and then S (seconds), D (days) or W (week). If no unit is specified, seconds is used. +# barSizeSetting:str - Specifies the size of the bars that will be returned (within IB/TWS listimits). +# Valid values include: +# 1 sec +# 5 secs +# 15 secs +# 30 secs +# 1 min +# 2 mins +# 3 mins +# 5 mins +# 15 mins +# 30 mins +# 1 hour +# 1 day +# whatToShow:str - Determines the nature of data beinging extracted. Valid values include: +# TRADES +# MIDPOINT +# BID +# ASK +# BID_ASK +# HISTORICAL_VOLATILITY +# OPTION_IMPLIED_VOLATILITY +# useRTH:int - Determines whether to return all data available during the requested time span, +# or only data that falls within regular trading hours. Valid values include: +# 0 - all data is returned even where the market in question was outside of its +# regular trading hours. +# 1 - only data within the regular trading hours is returned, even if the +# requested time span falls partially or completely outside of the RTH. +# formatDate: int - Determines the date format applied to returned bars. validd values include: +# 1 - dates applying to bars returned in the format: yyyymmdd{space}{space}hh:mm:dd +# 2 - dates are returned as a long integer specifying the number of seconds since +# 1/1/1970 GMT. +# chartOptions:TagValueList - For internal use only. Use default value XYZ. """ diff --git a/lib/ib/messages/outgoing/request_marketdata.rb b/lib/ib/messages/outgoing/request_market_data.rb similarity index 65% rename from lib/ib/messages/outgoing/request_marketdata.rb rename to lib/ib/messages/outgoing/request_market_data.rb index a3c97d0..bdf1f27 100644 --- a/lib/ib/messages/outgoing/request_marketdata.rb +++ b/lib/ib/messages/outgoing/request_market_data.rb @@ -1,39 +1,49 @@ - module IB module Messages module Outgoing extend Messages # def_message macros - # ==> details: https://interactivebrokers.github.io/tws-api/tick_types.html - # + RequestMarketData = + def_message [ 1, 11 ], :request_id, + [ :contract, :serialize_short ], + [ :contract, :serialize_legs, :market_data ], + [ :delta_neutral, false ], # delta neutral: we do not support that + [ :tick_list, ->(tick_list){ tick_list.is_a?(Array) ? tick_list.join(',') : (tick_list || '')}, [] ], + [ :snapshot, false ], + [ :regulatory_snapshot, false ], + [ :mkt_data_options, "" ] # changed to enable requests in V 10.19 ff + end + + # ==> details: https://interactivebrokers.github.io/tws-api/tick_types.html + # # @data={:id => int: ticker_id - Must be a unique value. When the market data # returns, it will be identified by this tag, - # if omitted, id-autogeneration process is performed + # if omitted its autogenerated # :contract => IB::Contract, requested contract. # :tick_list => String: comma delimited list of requested tick groups: # Group ID - Description - Requested Tick Types # 100 - Option Volume (currently for stocks) - 29, 30 # 101 - Option Open Interest (currently for stocks) - 27, 28 # 104 - Historical Volatility (currently for stocks) - 23 - # 105 - Average Opt Volume, # new 971 + # 105 - Average Opt Volume, # new 971 # 106 - Option Implied Volatility (impvolat) - 24 - # 107 (climpvlt) # new 971 - # 125 (Bond analytic data) # new 971 + # 107 (climpvlt) # new 971 + # 125 (Bond analytic data) # new 971 # 162 - Index Future Premium - 31 # 165 - Miscellaneous Stats - 15, 16, 17, 18, 19, 20, 21 - # 166 (CScreen) # new 971, + # 166 (CScreen) # new 971, # 221/220 - Creditman, Mark Price (used in TWS P&L computations) - 37 # 225 - Auction values (volume, price and imbalance) - 34, 35, 36 - # 232/221(Pl-price ) # new 971 + # 232/221(Pl-price ) # new 971 # 233 - RTVolume - 48 # 236 - Shortable (inventory) - 46 # 256 - Inventory - ? # 258 - Fundamental Ratios - 47 - # 291 - (ivclose) - # 292 - (Wide News) - # 293 - (TradeCount) - # 295 - (VolumeRate) - # 318 - (LastRTHT-Trade) + # 291 - (ivclose) + # 292 - (Wide News) + # 293 - (TradeCount) + # 295 - (VolumeRate) + # 318 - (LastRTHT-Trade) # 370 - (Participation Monitor) # 375 - RTTrdVolumne # 377 - CttTickTag @@ -47,11 +57,11 @@ module Outgoing # 411 - Realtime Historical Volatility - 58 # 428 - Monetary Close # 439 - MonitorTicTag - # 456/59 - IB Dividends, 4 comma separated values: 12 Month dividend, - # projected 12 Month dividend, - # next dividend date, - # next dividend value - # (use primary exchange instead of smart) + # 456/59 - IB Dividends, 4 comma separated values: 12 Month dividend, + # projected 12 Month dividend, + # next dividend date, + # next dividend value + # (use primary exchange instead of smart) # 459 - RTCLOSE # 460 - Bond Factor Multiplier # 499 - Fee and Rebate Ratge @@ -77,26 +87,16 @@ module Outgoing # 608(EMA N), # 614(EtfNavMisc(hight/low)), # 619(Creditman Slow Mark Price), - # 623(EtfFrozenNavLast(fznavlast) ## updated 2018/1/21 + # 623(EtfFrozenNavLast(fznavlast) ## updated 2018/1/21 # # :snapshot => bool: Check to return a single snapshot of market data and # have the market data subscription canceled. Do not enter any - # :tick_list values if you use snapshot. + # :tick_list values if you use snapshot. # # :regulatory_snapshot => bool - With the US Value Snapshot Bundle for stocks, # regulatory snapshots are available for 0.01 USD each. (applies on demo accounts as well) # :mktDataOptions => (TagValueList) For internal use only. - # Use default value XYZ. + # Use default value XYZ. # - RequestMarketData = - def_message [1, 11], :request_id, - [:contract, :serialize_short, :primary_exchange], # include primary exchange in request - [:contract, :serialize_legs, []], - [:contract, :serialize_under_comp, []], - [:tick_list, ->(tick_list){ tick_list.is_a?(Array) ? tick_list.join(',') : (tick_list || '')}, []], - [:snapshot, false], - [:regulatory_snapshot, false], - [:mkt_data_options, "XYZ"] - end end end diff --git a/lib/ib/messages/outgoing/request_market_depth.rb b/lib/ib/messages/outgoing/request_market_depth.rb new file mode 100644 index 0000000..dc58e83 --- /dev/null +++ b/lib/ib/messages/outgoing/request_market_depth.rb @@ -0,0 +1,57 @@ + +module IB + module Messages + module Outgoing + extend Messages # def_message macros + + ## actual Version supported is: 137 + ## changes: MIN_SERVER_VER_SMART_DEPTH: 146 --> insert 'is_smarth_depth' after 'num_rows' + ## then: 'is_smart_depth' (bool) has to be specified in CancelMarketDepth, too + # + # + + RequestMarketDepth = def_message( + [10, 5], + :request_id, # autogenerated if not specified + [:contract, :serialize, :option, :trading_class], # serialize_supershort], + :num_rows, + :is_smart_depth, + "" + ) # mktDataOptionsStr. ## not supported by api + + class RequestMarketDepth + def encode + ## create a proper request_id and erase :id and :ticker_id if nessesary + if self.class.properties?.include?(:request_id) + @data[:request_id] = @data[:id] || @data[:ticker_id] || @data[:request_id] || rand(9999) + @data[:id] = @data[:ticker_id] = nil + end + contract = @data[:contract] +# error "RequestMarketDepth requires a valid con-id" if contract.con_id.empty? + + [ + self.class.message_id, + self.class.version, + @data[:request_id], + contract.con_id, + contract.symbol, + contract[:sec_type], + contract.expiry, #last_trade_date_or_contract_month, + contract.strike, + contract.right == :none ? '' : contract.right, + contract.multiplier, + contract.exchange, + contract.primary_exchange, + contract.currency, + contract.local_symbol, + contract.trading_class, + @data[:num_rows], + @data[:is_smart_depth], + "", + "" + ] + end + end + end + end +end diff --git a/lib/ib/messages/outgoing/request_real_time_bars.rb b/lib/ib/messages/outgoing/request_real_time_bars.rb new file mode 100644 index 0000000..831e847 --- /dev/null +++ b/lib/ib/messages/outgoing/request_real_time_bars.rb @@ -0,0 +1,48 @@ +module IB + module Messages + module Outgoing + extend Messages # def_message macros + + + # data = { :id => ticker_id (int), + # :contract => Contract , + # :bar_size => int/Symbol? Currently only 5 second bars are supported, + # if any other value is used, an exception will be thrown., + # :data_type => Symbol: Determines the nature of data being extracted. + # :trades, :midpoint, :bid, :ask, :bid_ask, + # :historical_volatility, :option_implied_volatility, + # :option_volume, :option_open_interest + # - converts to "TRADES," "MIDPOINT," "BID," etc... + # :use_rth => int: 0 - all data available during the time span requested + # is returned, even data bars covering time intervals where the + # market in question was illiquid. 1 - only data within the + # "Regular Trading Hours" of the product in question is returned, + # even if the time span requested falls partially or completely + # outside of them. + # + # Version 3 + RequestRealTimeBars = def_message [ 50, 3 ], BarRequestMessage, + :request_id # autogenerated if not specified + + class RequestRealTimeBars + def parse data + data_type, bar_size, contract = super data + + size = data[:bar_size] || data[:size] + bar_size = 5 # only 5 sec bars are supported --> for future use ::> size.to_i + [data_type, bar_size, contract] + end + + def encode + data_type, bar_size, contract = parse @data + + [super, + contract.serialize_short(:primary_exchange), # include primary exchange in request + bar_size, + data_type.to_s.upcase, + @data[:use_rth] , + "" # not suported realtimebars option string + ] + end + end # RequestRealTimeBars + diff --git a/lib/ib/messages/outgoing/request_scanner_subscription.rb b/lib/ib/messages/outgoing/request_scanner_subscription.rb new file mode 100644 index 0000000..23e2117 --- /dev/null +++ b/lib/ib/messages/outgoing/request_scanner_subscription.rb @@ -0,0 +1,73 @@ +module IB + module Messages + module Outgoing + extend Messages # def_message macros + # Start receiving market scanner results through the ScannerData messages. + # @data = { :id => ticker_id (int), + # :number_of_rows => int: number of rows of data to return for a query. + # :instrument => The instrument type for the scan. Values include + # 'STK', - US stocks + # 'STOCK.HK' - Asian stocks + # 'STOCK.EU' - European stocks + # :location_code => Legal Values include: + # - STK.US - US stocks + # - STK.US.MAJOR - US stocks (without pink sheet) + # - STK.US.MINOR - US stocks (only pink sheet) + # - STK.HK.SEHK - Hong Kong stocks + # - STK.HK.ASX - Australian Stocks + # - STK.EU - European stocks + # :scan_code => The type of the scan, such as HIGH_OPT_VOLUME_PUT_CALL_RATIO. + # :above_price => double: Only contracts with a price above this value. + # :below_price => double: Only contracts with a price below this value. + # :above_volume => int: Only contracts with a volume above this value. + # :market_cap_above => double: Only contracts with a market cap above this + # :market_cap_below => double: Only contracts with a market cap below this value. + # :moody_rating_above => Only contracts with a Moody rating above this value. + # :moody_rating_below => Only contracts with a Moody rating below this value. + # :sp_rating_above => Only contracts with an S&P rating above this value. + # :sp_rating_below => Only contracts with an S&P rating below this value. + # :maturity_date_above => Only contracts with a maturity date later than this + # :maturity_date_below => Only contracts with a maturity date earlier than this + # :coupon_rate_above => double: Only contracts with a coupon rate above this + # :coupon_rate_below => double: Only contracts with a coupon rate below this + # :exclude_convertible => Exclude convertible bonds. + # :scanner_setting_pairs => Used with the scan_code to help further narrow your query. + # Scanner Setting Pairs are delimited by slashes, making + # this parameter open ended. Example is "Annual,true" - + # when used with 'Top Option Implied Vol % Gainers' scan + # would return annualized volatilities. + # :average_option_volume_above => int: Only contracts with average volume above this + # :stock_type_filter => Valid values are: + # 'ALL' (excludes nothing) + # 'STOCK' (excludes ETFs) + # 'ETF' (includes ETFs) } + # ------------ + # To learn all valid parameter values that a scanner subscription can have, + # first subscribe to ScannerParameters and send RequestScannerParameters message. + # Available scanner parameters values will be listed in received XML document. + RequestScannerSubscription = + def_message([22, 3], :request_id , + [:number_of_rows, -1], # was: EOL, + :instrument, + :location_code, + :scan_code, + :above_price, + :below_price, + :above_volume, + :market_cap_above, + :market_cap_below, + :moody_rating_above, + :moody_rating_below, + :sp_rating_above, + :sp_rating_below, + :maturity_date_above, + :maturity_date_below, + :coupon_rate_above, + :coupon_rate_below, + :exclude_convertible, + :average_option_volume_above, # ? + :scanner_setting_pairs, + :stock_type_filter) + end + end +end diff --git a/lib/ib/messages/outgoing/request_tick_by_tick_data.rb b/lib/ib/messages/outgoing/request_tick_by_tick_data.rb new file mode 100644 index 0000000..7866bc5 --- /dev/null +++ b/lib/ib/messages/outgoing/request_tick_by_tick_data.rb @@ -0,0 +1,21 @@ + +module IB + module Messages + module Outgoing + extend Messages # def_message macros + + + + RequestTickByTickData = + def_message [0, 97], :request_id, # autogenerated if not specified + [:contract, :serialize_short, :primary_exchange], # include primary exchange in request + :tick_type, # a string supported: "Last", "AllLast", "BidAsk" or "MidPoint". + # Server_version >= 140 + :number_of_ticks, # int + :ignore_size # bool + + CancelTickByTickData = + def_message [0, 98], :request_id + end + end +end diff --git a/lib/ib/messages/outgoing/request_tick_data.rb b/lib/ib/messages/outgoing/request_tick_data.rb deleted file mode 100644 index 6e19b4f..0000000 --- a/lib/ib/messages/outgoing/request_tick_data.rb +++ /dev/null @@ -1,21 +0,0 @@ - -module IB - module Messages - module Outgoing - extend Messages # def_message macros - - - - RequestTickByTickData = - def_message [0, 97], :request_id, # autogenerated if not specified - [:contract, :serialize_short, :primary_exchange], # include primary exchange in request - :tick_type, # a string supported: "Last", "AllLast", "BidAsk" or "MidPoint". - # Server_version >= 140 - :number_of_ticks, # int - :ignore_size # bool - - CancelTickByTickData = - def_message [0, 98], :request_id - end - end -end diff --git a/lib/ib/model.rb b/lib/ib/model.rb deleted file mode 100644 index 7c522e4..0000000 --- a/lib/ib/model.rb +++ /dev/null @@ -1,4 +0,0 @@ -# lightweigth tables are used -require 'ib/base_properties' -require 'ib/base' - IB::Model = IB::Base diff --git a/lib/ib/models.rb b/lib/ib/models.rb deleted file mode 100644 index 28907b5..0000000 --- a/lib/ib/models.rb +++ /dev/null @@ -1,14 +0,0 @@ - - require 'models/ib/account' - require 'models/ib/account_value' - require 'models/ib/contract_detail' - require 'models/ib/underlying' - require 'models/ib/contract' - require 'models/ib/order_state' - require 'models/ib/portfolio_value' - require 'models/ib/order' - require 'models/ib/combo_leg' - require 'models/ib/execution' - require 'models/ib/bar' - require 'models/ib/spread' - require 'models/ib/condition' diff --git a/lib/ib/order_condition.rb b/lib/ib/order_condition.rb new file mode 100644 index 0000000..b5f219d --- /dev/null +++ b/lib/ib/order_condition.rb @@ -0,0 +1,26 @@ +module IB + + + + + class OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages + # subclasses representing specialized condition types. + + Subclasses = Hash.new(OrderCondition) + Subclasses[1] = IB::PriceCondition + Subclasses[3] = IB::TimeCondition + Subclasses[5] = IB::ExecutionCondition + Subclasses[4] = IB::MarginCondition + Subclasses[6] = IB::VolumeCondition + Subclasses[7] = IB::PercentChangeCondition + + + # This builds an appropriate subclass based on its type + # + def self.make_from buffer + condition_type = buffer.read_int + OrderCondition::Subclasses[condition_type].make( buffer ) + end + end # class +end # module diff --git a/lib/ib/plugins.rb b/lib/ib/plugins.rb new file mode 100644 index 0000000..6d70ae0 --- /dev/null +++ b/lib/ib/plugins.rb @@ -0,0 +1,27 @@ +module IB + module Plugins + def activate_plugin *names + root= Pathname(__dir__).parent.parent + names.map{|y| y.to_s.gsub("_","-")}.each do |n| + unless @plugins.include? n + # root= base directory of the ib-api source + # plugins are defined in ib-api/plugins/ib + filename = root.join( "plugins", "ib", n+".rb" ) + if filename.exist? + if require filename + @plugins << n + true # return value + else + error "Could not load Plugin `#{n}` --> #{filename} " + end + else + error "Plugin `#{n}` not found in `plugins/ib/`" + nil + end + else + IB::Connection.logger.debug "Already activated plugin #{n}" + end + end + end + end +end diff --git a/lib/ib/prepare_data.rb b/lib/ib/prepare_data.rb new file mode 100644 index 0000000..193f60c --- /dev/null +++ b/lib/ib/prepare_data.rb @@ -0,0 +1,61 @@ +module IB + # includes methods from IB:.Support + # which adds a tws-method to + # - Array + # - Symbol + # - String + # - Numeric + # - TrueClass, FalseClass and NilClass + # + module PrepareData + using IB::Support + # First call the method #tws on the data-object + # + # Then transfom into an Array using the #Pack-Method + # + # The optional Block introduces a user-defined pattern to pack the data. + # + # Default is "Na*" + def prepare_message data + data = data.tws unless data.is_a?(String) && data[-1]== EOL + matrize = [data.size,data] + if block_given? # A user defined decoding-sequence is accepted via block + matrize.pack yield + else + matrize.pack "Na*" + end + end + + # The received package is decoded. The parameter (msg) is an Array + # + # The protocol is simple: Every Element is treated as Character. + # Exception: The first Element determines the expected length. + # + # The decoded raw-message can further modified by the optional block. + # + # The default is to instantiate a Hash: message_id becomes the key. + # The Hash is returned + # + # If a block is provided, no Hash is build and the modified raw-message is returned + def decode_message msg + m = Hash.new + while not msg.blank? + # the first item is the length + size = msg[ 0 .. 4 ].unpack( "N" ).first + msg = msg[ 4 .. -1 ] + # followed by a sequence of characters + message = msg.unpack( "A#{size}" ).first.split( "\0" ) + # DEBUG display raw decoded message on STDOUT + # STDOUT::puts "message: #{message}" + if block_given? + yield message + else + m[ message.shift.to_i ] = message + end + msg = msg[ size .. -1 ] + end + return m unless m == {} + end + + end +end diff --git a/lib/ib/raw_message_parser.rb b/lib/ib/raw_message_parser.rb new file mode 100644 index 0000000..5c97083 --- /dev/null +++ b/lib/ib/raw_message_parser.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module IB + # Convert data passed in from a TCP socket stream, and convert into raw messages. The messages + class RawMessageParser + HEADER_LNGTH = 4 + def initialize socket + @socket = socket + @data = String.new + end + + def each + append_new_data + + while valid_data? + # puts "looping: #{@data.inspect}" + + length = next_msg_length + validate_data_header(length) + + raw = grab_message(length) + validate_message_footer(raw, length) + msg = parse_message(raw, length) + remove_message + yield msg + end + end + + def valid_data? + # Make sure message length is available + return false unless length_data? + + # Based on the length, do we have + # enough data to process a full + # message? + return false unless enough_data? + + true + end + + # extract message and convert to + # an array split by null characters. + def grab_message(length) + @data.byteslice(HEADER_LNGTH, length) + end + + def parse_message(raw, length) + raw.unpack1("A#{length}").split("\0") + end + + def remove_message + length = next_msg_length + leftovers = @data.byteslice( length + HEADER_LNGTH..-1 ) + @data = if leftovers.nil? + String.new + else + leftovers + end + end + + def enough_data? + actual_lngth = next_msg_length + HEADER_LNGTH + if next_msg_length.nil? + Connection.current.logger.warn {"too little data --> #{@data} "} + false + else + @data.bytesize >= actual_lngth + end + end + + def length_data? + @data.bytesize > HEADER_LNGTH + end + + def next_msg_length + # can't check length if first 4 bytes don't exist + length = @data.byteslice(0..3).unpack1('N') + length.nil? ? 0 : length + end + + def append_new_data + @data += @socket.recvfrom(4096)[0] + end + + def validate_message_footer(msg, _length) + last = msg.bytesize + last_byte = msg.byteslice(last - 1, last) + raise 'Could not validate last byte' if last_byte.nil? + raise "Message has an invalid last byte. expecting \0, received: #{last_byte}" if last_byte != "\0" + end + + def validate_data_header(length) # disabled + # todo:: verify length according to the expected dataframe + # RequestHistoryData returns large datastreams + return true# if length <= 5000 + raise "Message is longer then max length (#{length}/5000)" + end + end +end diff --git a/lib/ib/socket.rb b/lib/ib/socket.rb index 3fc97e8..1109e96 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -1,133 +1,31 @@ -require 'socket' -module IBSupport - refine Array do - def tws - if blank? - nil.tws - else - self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil] - # elemets with empty array's are cut - # this is the desired behavior! - end - end - end - refine Symbol do - def tws - self.to_s.tws - end - end - refine String do - def tws - if empty? - IB::EOL - else - self[-1] == IB::EOL ? self : self+IB::EOL - end - end - end - - refine Numeric do - def tws - self.to_s.tws - end - end - - refine TrueClass do - def tws - 1.tws - end - end - - refine FalseClass do - def tws - 0.tws - end - end - - refine NilClass do - def tws - IB::EOL - end - end -end module IB - # includes methods from IBSupport - # which adds a tws-method to - # - Array - # - Symbol - # - String - # - Numeric - # - TrueClass, FalseClass and NilClass - # - module PrepareData - using IBSupport - # First call the method #tws on the data-object - # - # Then transfom into an Array using the #Pack-Method - # - # The optional Block introduces a user-defined pattern to pack the data. - # - # Default is "Na*" - def prepare_message data - data = data.tws unless data.is_a?(String) && data[-1]== EOL - matrize = [data.size,data] - if block_given? # A user defined decoding-sequence is accepted via block - matrize.pack yield - else - matrize.pack "Na*" - end - end - - # The received package is decoded. The parameter (msg) is an Array - # - # The protocol is simple: Every Element is treated as Character. - # Exception: The first Element determines the expected length. - # - # The decoded raw-message can further modified by the optional block. - # - # The default is to instantiate a Hash: message_id becomes the key. - # The Hash is returned - # - # If a block is provided, no Hash is build and the modified raw-message is returned - def decode_message msg - m = Hash.new - while not msg.blank? - # the first item is the length - size= msg[0..4].unpack("N").first - msg = msg[4..-1] - # followed by a sequence of characters - message = msg.unpack("A#{size}").first.split("\0") - if block_given? - yield message - else - m[message.shift.to_i] = message - end - msg = msg[size..-1] - end - return m unless block_given? - end - - end - - class IBSocket < TCPSocket - include PrepareData - using IBSupport + # includes methods from IB:.Support + # which adds a tws-method to + # - Array + # - Symbol + # - String + # - Numeric + # - TrueClass, FalseClass and NilClass + # + class Socket < TCPSocket + include IB::PrepareData + using IB::Support def initialising_handshake - v100_prefix = "API".tws.encode 'ascii' + v100_prefix = "API".tws.encode 'ascii' v100_version = self.prepare_message Messages::SERVER_VERSION write_data v100_prefix+v100_version - ## start tws-log - # [QO] INFO [JTS-SocketListener-49] - State: HEADER, IsAPI: UNKNOWN - # [QO] INFO [JTS-SocketListener-49] - State: STOP, IsAPI: YES - # [QO] INFO [JTS-SocketListener-49] - ArEServer: Adding 392382055 with id 2147483647 - # [QO] INFO [JTS-SocketListener-49] - eServersChanged: 1 - # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Starting new conversation with client on 127.0.0.1 - # [QO] INFO [JTS-EServerSocketNotifier-288] - Starting async queue thread - # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Server version is 136 - # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Client version is 136 - # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] is 3rdParty true - ## end tws-log + ## start tws-log + # [QO] INFO [JTS-SocketListener-49] - State: HEADER, IsAPI: UNKNOWN + # [QO] INFO [JTS-SocketListener-49] - State: STOP, IsAPI: YES + # [QO] INFO [JTS-SocketListener-49] - ArEServer: Adding 392382055 with id 2147483647 + # [QO] INFO [JTS-SocketListener-49] - eServersChanged: 1 + # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Starting new conversation with client on 127.0.0.1 + # [QO] INFO [JTS-EServerSocketNotifier-288] - Starting async queue thread + # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Server version is 136 + # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] Client version is 136 + # [QO] INFO [JTS-EServerSocket-287] - [2147483647:136:136:1:0:0:0:SYS] is 3rdParty true + ## end tws-log end @@ -135,9 +33,9 @@ def read_string string = self.gets(EOL) until string - # Silently ignores nils - string = self.gets(EOL) - sleep 0.1 + # Silently ignores nils + string = self.gets(EOL) + sleep 0.1 end string.chomp @@ -155,31 +53,31 @@ def send_messages *data self.syswrite prepare_message(data) rescue Errno::ECONNRESET => e Connection.logger.fatal{ "Data not accepted by IB \n - #{data.inspect} \n - Backtrace:\n "} + #{data.inspect} \n + Backtrace:\n "} Connection.logger.error e.backtrace end - def recieve_messages + def receive_messages begin - complete_message_buffer = [] - begin - # this is the blocking version of recv - buffer = self.recvfrom(4096)[0] - # STDOUT.puts "BUFFER:: #{buffer.inspect}" - complete_message_buffer << buffer - - end while buffer.size == 4096 - complete_message_buffer.join('') + complete_message_buffer = [] + begin + # this is the blocking version of recv + buffer = self.recvfrom(8192)[0] + # STDOUT.puts "BUFFER:: #{buffer.inspect}" + complete_message_buffer << buffer + + end while buffer.size == 8192 + complete_message_buffer.join('') rescue Errno::ECONNRESET => e - Connection.logger.fatal{ "Data Buffer is not filling \n - The Buffer: #{buffer.inspect} \n - Backtrace:\n - #{e.backtrace.join("\n") } " } - Kernel.exit + Connection.logger.fatal{ "Data Buffer is not filling \n + The Buffer: #{buffer.inspect} \n + Backtrace:\n + #{e.backtrace.join("\n") } " } + Kernel.exit end end - end # class IBSocket + end # class Socket end # module IB diff --git a/lib/ib/support.rb b/lib/ib/support.rb index 173e417..65ff039 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -1,177 +1,236 @@ +# Class-extensions only applied when data are read from the tws +# Array : read several formats +# Array, String, Symbol, true, false, nil : apply tws.method +# +# Apply through: module aaxx +# using IB::Support +# +module IB + module Support + + refine Array do + + def zero? + false + end + # Returns the integer. + # retuns nil otherwise or if no element is left on the stack + def read_int + i= self.shift rescue nil + i = i.to_i unless i.blank? # this includes conversion of string to zero(0) + i.is_a?( Integer ) && i != 2147483647 ? i : nil + + end + + def read_float + i= self.shift rescue nil + i = i.to_f unless i.blank? + + end + def read_decimal + i= self.shift rescue nil + i = i.to_d unless i.blank? + i.is_a?(Numeric) && i < IB::TWS_MAX ? i : nil # return nil, if a very large number is transmitted + end + + alias read_decimal_max read_decimal + + ## Values -1 and below indicate: Not computed (TickOptionComputation) + def read_decimal_limit_1 + i= read_decimal + i <= -1 ? nil : i + end + + ## Values -2 and below indicate: Not computed (TickOptionComputation) + def read_decimal_limit_2 + i= read_decimal + i <= -2 ? nil : i + end -module IBSupport - refine Array do - - def zero? - false - end - # Returns the integer. - # retuns nil otherwise or if no element is left on the stack - def read_int - i= self.shift rescue nil - i = i.to_i unless i.blank? # this includes conversion of string to zero(0) - i.is_a?( Integer ) ? i : nil - end - - def read_float - i= self.shift rescue nil - i = i.to_f unless i.blank? - - end - def read_decimal - i= self.shift rescue nil - i = i.to_d unless i.blank? - i.is_a?(Numeric) && i < IB::TWS_MAX ? i : nil # return nil, if a very large number is transmitted - end - - alias read_decimal_max read_decimal - - ## Values -1 and below indicate: Not computed (TickOptionComputation) - def read_decimal_limit_1 - i= read_float - i <= -1 ? nil : i - end - - ## Values -2 and below indicate: Not computed (TickOptionComputation) - def read_decimal_limit_2 - i= read_float - i <= -2 ? nil : i - end - - - def read_string - self.shift rescue "" - end - ## reads a string and proofs if NULL == IB::TWS_MAX is present. - ## in that case: returns nil. otherwise: returns the string - def read_string_not_null - r = read_string - rd = r.to_d unless r.blank? - rd.is_a?(Numeric) && rd >= IB::TWS_MAX ? nil : r - end - - def read_symbol - read_string.to_sym - end - - # convert xml into a hash - def read_xml - Ox.load( read_string(), mode: :hash_no_attrs) - end - - - def read_int_date - t= read_int + + def read_string + self.shift rescue "" + end + ## reads a string and checks if NULL == IB::TWS_MAX is present. + ## in that case: returns nil. otherwise: returns the string + def read_string_not_null + r = read_string + rd = r.to_d unless r.blank? + rd.is_a?(Numeric) && rd >= IB::TWS_MAX ? nil : r + end + + def read_symbol + read_string.to_sym + end + + # convert xml into a hash + def read_xml + Ox.load( read_string(), mode: :hash_no_attrs) + end + + + def read_int_date + t= read_int s= Time.at(t.to_i) - # s.year == 1970 --> data is most likely a date-string - s.year == 1970 ? Date.parse(t.to_s) : s - end - - def read_parse_date - Time.parse read_string - end - - def read_boolean - - v = self.shift rescue nil - case v - when "1" - true - when "0" - false - else nil - end - end - - - def read_datetime - the_string = read_string - the_string.blank? ? nil : DateTime.parse(the_string) - end - - def read_date - the_string = read_string - the_string.blank? ? nil : Date.parse(the_string) - end - # def read_array - # count = read_int - # end - - ## originally provided in socket.rb - # # Returns loaded Array or [] if count was 0# - # - # Without providing a Block, the elements are treated as string - def read_array hashmode:false, &block - count = read_int - case count - when 0 - [] - when nil - nil - else - count= count + count if hashmode - if block_given? - Array.new(count, &block) - else - Array.new( count ){ read_string } - end - end - end - # - # Returns a hash - # Expected Buffer-Format: - # count (of Hash-elements) - # count* key|Value - # Key's are transformed to symbols, values are treated as string - def read_hash - tags = read_array( hashmode: true ) # { |_| [read_string, read_string] } - result = if tags.nil? || tags.flatten.empty? - tags - else - interim = if tags.size.modulo(2).zero? - Hash[*tags.flatten] - else - Hash[*tags[0..-2].flatten] # omit the last element - end - # symbolize Hash - Hash[interim.map { |k, v| [k.to_sym, v] unless k.nil? }.compact] - end - end - # + # s.year == 1970 --> data is most likely a date-string + s.year == 1970 ? Date.parse(t.to_s) : s + end + + def read_parse_date + Time.parse read_string + end + + def read_boolean + + v = self.shift rescue nil + case v + when "1" + true + when "0" + false + else nil + end + end + + + def read_datetime + the_string = read_string + the_string.blank? ? nil : DateTime.parse(the_string) + end + + def read_date + the_string = read_string + the_string.blank? ? nil : Date.parse(the_string) + end + # def read_array + # count = read_int + # end + + ## originally provided in socket.rb + # # Returns loaded Array or [] if count was 0# + # + # Without providing a Block, the elements are treated as string + def read_array hashmode:false, &block + count = read_int + case count + when 0 + [] + when nil + nil + else + count= count + count if hashmode + if block_given? + Array.new(count, &block) + else + Array.new( count ){ read_string } + end + end + end + # + # Returns a hash + # Expected Buffer-Format: + # count (of Hash-elements) + # count* key|Value + # Key's are transformed to symbols, values are treated as string + def read_hash + tags = read_array( hashmode: true ) # { |_| [read_string, read_string] } + result = if tags.nil? || tags.flatten.empty? + tags # {} + else + interim = if tags.size.modulo(2).zero? + Hash[*tags.flatten] + else + Hash[*tags[0..-2].flatten] # omit the last element + end + # symbolize Hash + interim.map { |k, v| [k.to_sym, v] unless k.nil? }.compact.to_h + end + end + # def read_contract # read a standard contract and return als hash - { con_id: read_int, - symbol: read_string, - sec_type: read_string, - expiry: read_string, - strike: read_decimal, - right: read_string, - multiplier: read_int, - exchange: read_string, - currency: read_string, + { con_id: read_int, + symbol: read_string, + sec_type: read_string, + expiry: read_string, + strike: read_decimal, + right: read_string, + multiplier: read_decimal, + exchange: read_string, + currency: read_string, local_symbol: read_string, - trading_class: read_string } # new Version 8 + trading_class: read_string } end - def read_bar # read a Historical data bar -# ** historicalDataUpdate: time open close high low ** covered hier + def read_bar # read a Historical data bar +# ** historicalDataUpdate: time open close high low ** covered here # historicalData time open high low close <- covered in messages/incomming { :time => read_int_date, # conversion of epoche-time-integer to Dateime # requires format_date in request to be "2" # (outgoing/bar_requests # RequestHistoricalData#Encoding) - :open => read_float, - :close => read_float, - :high => read_float, - :low => read_float, - :wap => read_float, - :volume => read_int, + :open => read_decimal, + :close => read_decimal, + :high => read_decimal, + :low => read_decimal, + :wap => read_decimal, + :volume => read_int, # :has_gaps => read_string, # only in ServerVersion < 124 - :trades => read_int } + :trades => read_int } + + end + + alias read_bool read_boolean + + def tws + if blank? + nil.tws + else + self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil] + # elements with empty array's are cut + # this is the desired behavior! + end + end + end # refining array + refine Symbol do + def tws + self.to_s.tws + end + end + refine String do + def tws + if empty? + IB::EOL + else + self[-1] == IB::EOL ? self : self+IB::EOL + end + end + end + + refine Numeric do + def tws + self.to_s.tws end + end + refine TrueClass do + def tws + 1.tws + end + end + + refine FalseClass do + def tws + 0.tws + end + end + + refine NilClass do + def tws + IB::EOL + end + end +end - alias read_bool read_boolean - end end diff --git a/lib/ib/version.rb b/lib/ib/version.rb index 503dee5..c6543dc 100644 --- a/lib/ib/version.rb +++ b/lib/ib/version.rb @@ -2,5 +2,5 @@ module IB VERSION_FILE = Pathname.new(__FILE__).dirname + '../../VERSION' # :nodoc: - VERSION = VERSION_FILE.exist? ? VERSION_FILE.read.strip : nil + Version = VERSION = VERSION_FILE.exist? ? VERSION_FILE.read.strip : nil end diff --git a/lib/logging.rb b/lib/logging.rb deleted file mode 100644 index 09aecd3..0000000 --- a/lib/logging.rb +++ /dev/null @@ -1,45 +0,0 @@ -#module Kernel -# private -# def this_method_name -# caller[0] =~ /`([^']*)'/ and $1 -# end -# see also __method__ and __callee__ -#end - - - -module Support - module Logging - def self.included(base) - base.extend ClassMethods - base.send :define_method, :logger do - base.logger - end - end - - module ClassMethods - def logger - @logger - end - - def logger=(logger) - @logger = logger - end - - def configure_logger(log=nil) - if log - @logger = log - else - @logger = Logger.new(STDOUT) - @logger.level = Logger::INFO - @logger.formatter = proc do |severity, datetime, progname, msg| - # "#{datetime.strftime("%d.%m.(%X)")}#{"%5s" % severity}->#{msg}\n" - "#{"%1s" % severity[0]}: #{msg}\n" - end - @logger.debug "------------------------------ start logging ----------------------------" - end # branch - end # def - end # module ClassMethods - end # module Logging -end # module Support - diff --git a/lib/models/ib/account.rb b/lib/models/ib/account.rb deleted file mode 100644 index a8bfb3c..0000000 --- a/lib/models/ib/account.rb +++ /dev/null @@ -1,72 +0,0 @@ -module IB - class Account < IB::Model - include BaseProperties - # attr_accessible :alias, :account, :connected - - prop :account, # String - :alias, # - :type, - :last_updated, - :connected => :bool - - - - validates_format_of :account, :with => /\A[D]?[UF]{1}\d{5,8}\z/ , :message => 'should be (X)X00000' - - # in tableless mode the scope is ignored - - has_many :account_values - has_many :portfolio_values - has_many :contracts - has_many :orders - has_many :focuses - - def default_attributes - super.merge account: 'X000000' - super.merge alias: '' - super.merge type: 'Account' - super.merge connected: false - end - - def logger #nodoc# - Connection.logger - end - - def print_type #nodoc# - (test_environment? ? "demo_" : "") + ( user? ? "user" : "advisor" ) - end - - def advisor? - !!(type =~ /Advisor/ || account =~ /\A[D]?[F]{1}/) - end - - def user? - !!(type =~ /User/ || account =~ /\A[D]?[U]{1}/) - end - - def test_environment? - !!(account =~ /^[D]{1}/) - end - - def == other - super(other) || - other.is_a?(self.class) && account == other.account - end - - def to_human - a = if self.alias.present? && self.alias != account - " alias: "+ self.alias - else - "" - end - "<#{print_type} #{account}#{a}>" - end - - def name #nodoc# - self.alias.present? ? self.alias : account - end - -# alias :id :account -end # class - -end # module diff --git a/lib/models/ib/condition.rb b/lib/models/ib/condition.rb deleted file mode 100644 index 28582be..0000000 --- a/lib/models/ib/condition.rb +++ /dev/null @@ -1,245 +0,0 @@ -require 'ib/support' -module IB - class OrderCondition < IB::Model - include BaseProperties - - - prop :operator, # 1 -> " >= " , 0 -> " <= " see /lib/ib/constants # 338f - :conjunction_connection, # "o" -> or "a" - :contract - def self.verify_contract_if_necessary c - c.con_id.to_i.zero? ||( c.primary_exchange.blank? && c.exchange.blank?) ? c.verify! : c - end - def condition_type - error "condition_type method is abstract" - end - def default_attributes - super.merge( operator: ">=" , conjunction_connection: :and ) - end - - def serialize_contract_by_con_id - [ contract.con_id , contract.primary_exchange.presence || contract.exchange ] - end - - def serialize - [ condition_type, self[:conjunction_connection] ] - end - end - - - - class PriceCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - prop :price, - :trigger_method # see /models/ib/order.rb# 51 ff and /lib/ib/constants # 210 ff - - def default_attributes - super.merge( :trigger_method => :default ) - end - - def condition_type - 1 - end - - def self.make buffer - m= self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int, - price: buffer.read_decimal - - the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string - m.contract = the_contract - m.trigger_method = buffer.read_int - m - - end - - def serialize - super << self[:operator] << price << serialize_contract_by_con_id << self[:trigger_method] - end - - # dsl: PriceCondition.fabricate some_contract, ">=", 500 - def self.fabricate contract, operator, price - error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator - self.new operator: operator, - price: price.to_i, - contract: verify_contract_if_necessary( contract ) - end - - end - - class TimeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - prop :time - - def condition_type - 3 - end - - def self.make buffer - self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int, - time: buffer.read_parse_date - end - - def serialize - t = self[:time] - if t.is_a?(String) && t =~ /^\d{8}\z/ # expiry-format yyymmmdd - self.time = DateTime.new t[0..3],t[4..5],t[-2..-1] - end - serialized_time = case self[:time] # explicity formatting of time-object - when String - self[:time] - when DateTime - self[:time].gmtime.strftime("%Y%m%d %H:%M:%S %Z") - when Date, Time - self[:time].strftime("%Y%m%d %H:%M:%S") - end - - super << self[:operator] << serialized_time - end - - def self.fabricate operator, time - self.new operator: operator, - time: time - end - end - - class ExecutionCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - - def condition_type - 5 - end - - def self.make buffer - m =self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int - - the_contract = IB::Contract.new sec_type: buffer.read_string, - exchange: buffer.read_string, - symbol: buffer.read_string - m.contract = the_contract - m - end - - def serialize - super << contract[:sec_type] <<(contract.primary_exchange.presence || contract.exchange) << contract.symbol - end - - def self.fabricate contract - self.new contract: verify_contract_if_necessary( contract ) - end - - end - - class MarginCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - - prop :percent - - def condition_type - 4 - end - - def self.make buffer - self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int, - percent: buffer.read_int - - end - - def serialize - super << self[:operator] << percent - end - def self.fabricate operator, percent - error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator - self.new operator: operator, - percent: percent - end - end - - - class VolumeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - - prop :volume - - def condition_type - 6 - end - - def self.make buffer - m = self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int, - volumne: buffer.read_int - - the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string - m.contract = the_contract - m - end - - def serialize - - super << self[:operator] << volume << serialize_contract_by.con_id - end - - # dsl: VolumeCondition.fabricate some_contract, ">=", 50000 - def self.fabricate contract, operator, volume - error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator - self.new operator: operator, - volume: volume, - contract: verify_contract_if_necessary( contract ) - end - end - - class PercentChangeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - prop :percent_change - - def condition_type - 7 - end - - def self.make buffer - m = self.new conjunction_connection: buffer.read_string, - operator: buffer.read_int, - percent_change: buffer.read_decimal - - the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string - m.contract = the_contract - m - end - - def serialize - super << self[:operator] << percent_change << serialize_contract_by_con_id - - end - # dsl: PercentChangeCondition.fabricate some_contract, ">=", "5%" - def self.fabricate contract, operator, change - error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator - self.new operator: operator, - percent_change: change.to_i, - contract: verify_contract_if_necessary( contract ) - end - end - class OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages - # subclasses representing specialized condition types. - - Subclasses = Hash.new(OrderCondition) - Subclasses[1] = IB::PriceCondition - Subclasses[3] = IB::TimeCondition - Subclasses[5] = IB::ExecutionCondition - Subclasses[4] = IB::MarginCondition - Subclasses[6] = IB::VolumeCondition - Subclasses[7] = IB::PercentChangeCondition - - - # This builds an appropriate subclass based on its type - # - def self.make_from buffer - condition_type = buffer.read_int - OrderCondition::Subclasses[condition_type].make( buffer ) - end - end # class -end # module diff --git a/lib/models/ib/future.rb b/lib/models/ib/future.rb deleted file mode 100644 index cf025fd..0000000 --- a/lib/models/ib/future.rb +++ /dev/null @@ -1,15 +0,0 @@ -#require 'models/ib/contract' -module IB - class Future < Contract - validates_format_of :sec_type, :with => /\Afuture\z/, - :message => "should be a Future" - def default_attributes - super.merge :sec_type => :future, currency:'USD' - end - def to_human - "" - end - - end - end - diff --git a/lib/models/ib/option.rb b/lib/models/ib/option.rb deleted file mode 100644 index 607bb72..0000000 --- a/lib/models/ib/option.rb +++ /dev/null @@ -1,84 +0,0 @@ -#require_relative 'contract' -require_relative 'option_detail' - -module IB - class Option < Contract - - validates_numericality_of :strike, :greater_than => 0 - validates_format_of :sec_type, :with => /\Aoption\z/, - :message => "should be an option" - validates_format_of :local_symbol, :with => /\A\w+\s*\d{6}[pcPC]\d{8}$|\A\z/, - :message => "invalid OSI code" - validates_format_of :right, :with => /\Aput$|^call\z/, - :message => "should be put or call" - - - # introduce Option.greek with reference to IB::OptionDetail-dataset - # - has_one :greek , as: :option_detail - # For Options, this is contract's OSI (Option Symbology Initiative) name/code - alias osi local_symbol - - def osi= value - # Normalize to 21 char - self.local_symbol = value.sub(/ /, ' '*(22-value.size)) - end - - # Make valid IB Contract definition from OSI (Option Symbology Initiative) code. - # NB: Simply making a new Contract with *local_symbol* (osi) property set to a - # valid OSI code works just as well, just do NOT set *expiry*, *right* or - # *strike* properties in this case. - # This class method provided as a backup and shows how to analyse OSI codes. - def self.from_osi osi - - # Parse contract's OSI (OCC Option Symbology Initiative) code - args = osi.match(/(\w+)\s?(\d\d)(\d\d)(\d\d)([pcPC])(\d+)/).to_a.drop(1) - symbol = args.shift - year = 2000 + args.shift.to_i - month = args.shift.to_i - day = args.shift.to_i - right = args.shift.upcase - strike = args.shift.to_i/1000.0 - - # Set correct expiry date - IB expiry date differs from OSI if expiry date - # falls on Saturday (see https://github.com/arvicco/option_mower/issues/4) - expiry_date = Time.utc(year, month, day) - expiry_date = Time.utc(year, month, day-1) if expiry_date.wday == 6 - - new :symbol => symbol, - :exchange => "SMART", - :expiry => expiry_date.to_ib[2..7], # YYMMDD - :right => right, - :strike => strike - end - - def default_attributes - super.merge :sec_type => :option - #self[:description] ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}" - end - def == other - super(other) || ( # finish positive, if contract#== is true - # otherwise, we most probably compare the response from IB with our selfmade input - exchange == other.exchange && - include_expired == other.include_expired && - sec_type == other.sec_type && - multiplier == other.multiplier && - strike == other.strike && - right == other.right && - multiplier == other.multiplier && - expiry == other.expiry ) - - end - - def to_human - "" - end - - end # class Option - - class FutureOption < Option - def default_attributes - super.merge :sec_type => :futures_option - end - end -end # module IB diff --git a/lib/models/ib/spread.rb b/lib/models/ib/spread.rb deleted file mode 100644 index d044321..0000000 --- a/lib/models/ib/spread.rb +++ /dev/null @@ -1,183 +0,0 @@ -module IB - if defined?(Spread) - puts "Bag already a #{defined?(Spread)}" - -# puts Spread.ancestors - IB.send(:remove_const, 'Spread') - end - class Spread < Bag - has_many :legs - - using IBSupport - -=begin -Parameters: front: YYYMM(DD) - back: {n}w, {n}d or YYYYMM(DD) - -Adds (or substracts) relative (back) measures to the front month, just passes absolute YYYYMM(DD) value - - front: 201809 back: 2m (-1m) --> 201811 (201808) - front: 20180908 back: 1w (-1w) --> 20180918 (20180902) -=end - - def self.transform_distance front, back - # Check Format of back: 201809 --> > 200.000 - # 20180989 ---> 20.000.000 - start_date = front.to_i < 20000000 ? Date.strptime(front.to_s,"%Y%m") : Date.strptime(front.to_s,"%Y%m%d") - nb = if back.to_i > 200000 - back.to_i - elsif back[-1] == "w" && front.to_i > 20000000 - start_date + (back.to_i * 7) - elsif back[-1] == "m" && front.to_i > 200000 - start_date >> back.to_i - else - error "Wrong date #{back} required format YYYMM, YYYYMMDD ord {n}w or {n}m" - end - if nb.is_a?(Date) - if back[-1]=='w' - nb.strftime("%Y%m%d") - else - nb.strftime("%Y%m") - end - else - nb - end - end # def - - def to_human - self.description - end - - def calculate_spread_value( array_of_portfolio_values ) - array_of_portfolio_values.map{|x| x.send yield }.sum if block_given? - end - - def fake_portfolio_position( array_of_portfolio_values ) - calculate_spread_value= ->( a_o_p_v, attribute ) do - a_o_p_v.map{|x| x.send attribute }.sum - end - ar=array_of_portfolio_values - IB::PortfolioValue.new contract: self, - average_cost: calculate_spread_value[ar, :average_cost], - market_price: calculate_spread_value[ar, :market_price], - market_value: calculate_spread_value[ar, :market_value], - unrealized_pnl: calculate_spread_value[ar, :unrealized_pnl], - realized_pnl: calculate_spread_value[ar, :realized_pnl], - position: 0 - - end - - - # adds a leg to any spread - # - # Parameter: - # contract: Will be verified. Contract.essential is added to legs-array - # action: :buy or :sell - # weight: - # ratio: - # - # Default: action: :buy, weight: 1 - - def add_leg contract, **leg_params - error "need a IB::Contract as first argument" unless contract.is_a? IB::Contract - self.legs << contract - error "cannot add leg if no con_id is provided" if contract.con_id.blank? - # weigth = 1 --> sets Combo.side to buy and overwrites the action statement -# leg_params[:weight] = 1 unless leg_params.key?(:weight) || leg_params.key?(:ratio) - self.combo_legs << ComboLeg.new( contract.attributes.slice( :con_id, :exchange ).merge( leg_params )) - self.description = "#{description.nil? ? "": description} added #{contract.to_human}" rescue "Spread: #{contract.to_human}" - - self # return value to enable chaining - - - end - - # removes the contract from the spread definition - # - def remove_leg contract_or_position = nil - contract = if contract_or_position.is_a? (IB::Contract) - contract_or_position - elsif contract_or_position.is_a? Numeric - legs.at contract_or_position - else - error "Specify a contract to be removed or the position in the legs-array as parameter to remove a leg" - end - the_con_id = contract.verify.first &.con_id - error "Invalid Contract specified" unless the_con_id.is_a? Numeric - legs.delete_if { |x| x.con_id == the_con_id } - combo_legs.delete_if { |x| x.con_id == the_con_id } - self.description = description + " removed #{contract.to_human}" - self # make method chainable - end - -# essentail -# effectivley clones the object -# - def essential - the_es = self.class.new invariant_attributes - the_es.legs = legs.map{|y| IB::Contract.build y.invariant_attributes} - the_es.combo_legs = combo_legs.map{|y| IB::ComboLeg.new y.invariant_attributes } - the_es.description = description - the_es # return - end - - def multiplier - (legs.map(&:multiplier).sum/legs.size).to_i - end - - # provide a negative con_id - def con_id - if attributes[:con_id].present? && attributes[] < 0 - attributes[:con_id] - else - -legs.map{ |x| x.is_a?(String) ? x.expand.con_id : x.con_id}.sum - end - end - - - def non_guaranteed= x - super.merge combo_params: [ ['NonGuaranteed', x] ] - end - - - def non_guaranteed - combo_params['NonGuaranteed'] - end -# optional: specify default order prarmeters for all spreads -# def order_requirements -# super.merge symbol: symbol -# end - - - def as_table - t= Terminal::Table.new title: description[1..-2] , - headings: table_header, - - style: { border: :unicode } - - t.add_row table_row - legs.each{ |y| t.add_row y.table_row } - t.render - - end - - def self.build_from_json container - read_leg = ->(a) do - IB::ComboLeg.new :con_id => a.read_int, - :ratio => a.read_int, - :action => a.read_string, - :exchange => a.read_string - - end - object= self.new container['Spread'].clone.read_contract - object.legs = container['legs'].map{|x| IB::Contract.build x.clone.read_contract} - object.combo_legs = container['combo_legs'].map{ |x| read_leg[ x.clone ] } - object.description = container['misc'].clone.read_string - object - - end - - end - - -end diff --git a/lib/models/ib/stock.rb b/lib/models/ib/stock.rb deleted file mode 100644 index e03a878..0000000 --- a/lib/models/ib/stock.rb +++ /dev/null @@ -1,22 +0,0 @@ -#require_relative 'contract' -module IB - class Stock < IB::Contract - validates_format_of :sec_type, :with => /\Astock\z/, - :message => "should be a Stock" - validates_format_of :symbol, with: /\A.*\z/, - message: 'should not be blank' - def default_attributes - super.merge :sec_type => :stock, currency:'USD', exchange:'SMART' - end - - def to_human - att = [ symbol, - currency, ( exchange == 'SMART' ? nil: exchange ), - (primary_exchange.present? && !primary_exchange.empty? ? primary_exchange : nil), - @description.present? ? " (#{@description}) " : nil, - ].compact - "" - end - - end -end diff --git a/lib/models/ib/vertical.rb b/lib/models/ib/vertical.rb deleted file mode 100644 index 83d11d9..0000000 --- a/lib/models/ib/vertical.rb +++ /dev/null @@ -1,96 +0,0 @@ -require_relative 'contract' - - -module IB - class Vertical < Spread - -=begin -Macro-Class to simplify the definition of Vertical-Spreads - -Initialize with - - calendar = IB::Vertical.new underlying: Symbols::Index.stoxx, - buy: 3000, sell: 2900, right: :put - expiry: 201901, back: 291903 - or - - master = IB::Option.new symbol: :Estx50, right: :put, multiplier: 10, exchange: 'DTB', currency: 'EUR' - strike: 3000, expiry: 201812 - calendar = IB::Vertical.new sell: master, buy: 3100 - -=end - - - - def initialize master=nil, # provides strike, front-month, right, trading-class - underlying: nil, - right: :put, - expiry: IB::Symbols::Futures.next_expiry, - buy: 0 , # has to be specified - sell: 0, - # trading_class: nil, - **args # trading-class and others - - master_option, side, msg = if master.present? - if master.is_a?(IB::Option) - [ master.essential,-1, nil ] - else - [ nil, 0,"First Argument is no IB::Option" ] - end - elsif buy.is_a?(IB::Option) && !sell.zero? - [ buy.essentail,1, nil ] - elsif sell.is_a?(IB::Option) && !buy.zero? - [ sell.essential, -1, nil ] - elsif underlying.present? - if underlying.is_a?(IB::Contract) - master = IB::Option.new underlying.attributes.slice( :currency, :symbol, :exchange ).merge(args) - master.sec_type = 'FOP' if underlying.is_a?(IB::Future) - master.strike, master.expiry, master.right = buy, expiry, right - [master, 1, buy.to_i >0 && sell.to_i >0 ? nil : "buy and sell strikes have to be specified"] - else - [nil, 0, "Underlying has to be an IB::Contract"] - end - else - [ nil, 0, "Required parameters: Master-Option or Underlying, buy and sell-strikes" ] - end - - error msg, :args, nil if msg.present? - master_option.trading_class = args[:trading_class] if args[:trading_class].present? - l=[] ; master_option.verify{|x| x.contract_detail = nil; l << x } - if l.empty? - error "Invalid Parameters. No Contract found #{master_option.to_human}" - elsif l.size > 2 - Connection.logger.error "ambigous contract-specification: #{l.map(&:to_human).join(';')}" - available_trading_classes = l.map( &:trading_class ).uniq - if available_trading_classes.size >1 - error "Refine Specification with trading_class: #{available_trading_classes.join('; ')} (details in log)" - else - error "Respecify expiry, verification reveals #{l.size} contracts (only 2 are allowed) #{master_option.to_human}" - end - end - - master_option.strike = side ==1 ? sell : buy - master_option.verify{|x| x.contract_detail = nil; l << x } - error "Two legs are required, \n Verifiying the master-option exposed #{l.size} legs" unless l.size ==2 - - master_option.exchange ||= l.first.exchange - master_option.currency ||= l.first.currency - - # i=0 + side = -1 --> -1 sell - # i=0 + side = 1 --> 1 buy - # i=1 + side = -1 --> 0 buy - # i.1 + side = 1 --> 2 sell - c_l = l.map.with_index{ |l,i| ComboLeg.new con_id: l.con_id, action: i+side ==2 || i+side <0 ? :sell : :buy , exchange: l.exchange, ratio: 1 } - - super exchange: master_option.exchange, - symbol: master_option.symbol.to_s, - currency: master_option.currency, - legs: l, - combo_legs: c_l - end - def to_human - x= [ combo_legs.map(&:weight) , legs.map( &:strike )].transpose - "" - end - end -end diff --git a/lib/requires.rb b/lib/requires.rb deleted file mode 100644 index 67c6632..0000000 --- a/lib/requires.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors.rb' -require 'extensions/class-extensions' - -require 'terminal-table' - -require 'ib/version' -require 'ib/errors' -require 'ib/constants' -require 'ib/connection' - -# An external model- or database-driver provides the base class for models -# if the constant DB is defined -# -# basically IB::Model has to be assigned to the substitute base class -# the database-driver requires models and messages at the appropoate time -unless defined?(DB) - require 'ib/model' - require 'ib/models' - require 'ib/messages' -end diff --git a/lib/ib/server_versions.rb b/lib/server_versions.rb similarity index 69% rename from lib/ib/server_versions.rb rename to lib/server_versions.rb index 2f45fe7..5d341ce 100644 --- a/lib/ib/server_versions.rb +++ b/lib/server_versions.rb @@ -10,7 +10,7 @@ The known server versions. =end -known_servers = { +KNOWN_SERVERS = { #min_server_ver_real_time_bars => 34, #min_server_ver_scale_orders => 35, #min_server_ver_snapshot_mkt_data => 35, @@ -19,7 +19,7 @@ #min_server_ver_contract_conid => 37, :min_server_ver_pta_orders => 39, :min_server_ver_fundamental_data => 40, -:min_server_ver_under_comp => 40, +:min_server_ver_delta_neutral => 40, :min_server_ver_contract_data_chain => 40, :min_server_ver_scale_orders2 => 40, :min_server_ver_algo_orders => 41, @@ -99,16 +99,47 @@ :min_server_ver_order_container => 145, ### > Version Field in Order dropped :min_server_ver_smart_depth => 146, :min_server_ver_remove_null_all_casting => 147, -:min_server_ver_d_peg_orders => 148 - - - - +:min_server_ver_d_peg_orders => 148, +:min_server_ver_mkt_depth_prim_exchange => 149, +:min_server_ver_completed_orders => 150, +:min_server_ver_price_mgmt_algo => 151, +:min_server_ver_stock_type => 152, +:min_server_ver_encode_msg_ascii7 => 153, +:min_server_ver_send_all_family_codes => 154, +:min_server_ver_no_default_open_close => 155, +:min_server_ver_price_based_volitility => 156, +:min_server_ver_replace_fa_end => 157, +:min_server_ver_duration => 158, +:min_server_ver_market_data_in_shares => 159, +:min_server_ver_post_to_ats => 160, +:min_server_ver_wshe_calendar => 161, +:min_server_ver_auto_cancel_parent => 162, +:min_server_ver_fractional_size_support => 163, +:min_server_ver_size_rules => 164, +:min_server_ver_historical_schedule => 165, +:min_server_ver_advanced_order_reject => 166, +:min_server_ver_user_info => 167, +:min_server_ver_crypto_aggregated_trades => 168, +:min_server_ver_manual_order_time => 169, +:min_server_ver_pegbest_pegmid_offsets => 170, +:min_server_ver_wsh_event_data_filters => 171, +:min_server_ver_ipo_prices => 172, +:min_server_ver_wsh_event_data_filters_date => 173, +:min_server_ver_instrument_timezone => 174, +:min_server_ver_hmds_market_data_in_shares => 175, +:min_server_ver_bond_issuerid => 176, +:min_server_ver_fa_profile_desupport => 177, +:min_server_ver_pending_price_revision => 178, +:min_server_ver_fund_data_fields => 179, +:min_server_ver_manual_order_time_exercise_options => 180, +:min_server_ver_open_order_ad_strategy => 181, +:min_server_ver_last_trade_date => 182, +:min_server_ver_customer_account => 183, +:min_server_ver_professional_customer => 184 } # 100+ messaging */ # 100 = enhanced handshake, msg length prefixes MIN_CLIENT_VER = 100 -MAX_CLIENT_VER = 137 #known_servers[:min_server_ver_d_peg_orders] - +MAX_CLIENT_VER = KNOWN_SERVERS[:min_server_ver_historical_schedule] # 165 # imessages/outgoing/request_tick_Data is prepared for change to ver. 140 , its commented for now diff --git a/lib/support/array_function.rb b/lib/support/array_function.rb new file mode 100644 index 0000000..b230976 --- /dev/null +++ b/lib/support/array_function.rb @@ -0,0 +1,28 @@ +module Support + module ArrayFunction + def save_insert item, key, overwrite = true + member = find { |entry| entry[ key ] == item[ key] } + if member + self[ index( member ) ] = item if overwrite + else + self << item + end + self # always returns the array + end + + # performs [ [ array ] & [ array ] & [..] ].first + def intercept + a = self.dup + s = a.pop + while a.present? + s = s & a.pop + end + s.first unless s.nil? # return_value (or nil) + end + end # module +end # module + +class Array + include Support::ArrayFunction +end + diff --git a/lib/support/logging.rb b/lib/support/logging.rb new file mode 100644 index 0000000..d5c0fd7 --- /dev/null +++ b/lib/support/logging.rb @@ -0,0 +1,45 @@ +#module Kernel +# private +# def this_method_name +# caller[0] =~ /`([^']*)'/ and $1 +# end +# see also __method__ and __callee__ +#end + + + +module Support + module Logging + def self.included(base) + base.extend ClassMethods + base.send :define_method, :logger do + base.logger + end + end + + module ClassMethods + def logger + @logger + end + + def logger=(logger) + @logger = logger + end + + def configure_logger(log=nil) + if log + @logger = log + else + @logger = ::Logger.new(STDOUT) + @logger.level = ::Logger::INFO + @logger.formatter = proc do |severity, datetime, progname, msg| + # "#{datetime.strftime("%d.%m.(%X)")}#{"%5s" % severity}->#{msg}\n" + "#{"%1s" % severity[0]}: #{msg}\n" + end + @logger.debug "------------------------------ start logging ----------------------------" + end # branch + end # def + end # module ClassMethods + end # module Logging +end # module Support + diff --git a/models/ib/account.rb b/models/ib/account.rb new file mode 100644 index 0000000..67d2284 --- /dev/null +++ b/models/ib/account.rb @@ -0,0 +1,72 @@ +module IB + class Account < IB::Base + include BaseProperties + # attr_accessible :alias, :account, :connected + + prop :account, # String + :alias, # + :type, + :last_updated, + :connected => :bool + + + + validates_format_of :account, :with => /\A[D]?[UF]{1}\d{5,8}\z/ , :message => 'should be (X)X00000' + + # in tableless mode the scope is ignored + + has_many :account_values + has_many :portfolio_values + has_many :contracts + has_many :orders + has_many :focuses + + def default_attributes + super.merge account: 'X000000' + super.merge alias: '' + super.merge type: 'Account' + super.merge connected: false + end + + def logger #nodoc# + Connection.logger + end + + def print_type #nodoc# + (test_environment? ? "demo_" : "") + ( user? ? "user" : "advisor" ) + end + + def advisor? + !!(type =~ /Advisor/ || account =~ /\A[D]?[F]{1}/) + end + + def user? + !!(type =~ /User/ || account =~ /\A[D]?[U]{1}/) + end + + def test_environment? + !!(account =~ /^[D]{1}/) + end + + def == other + super(other) || + other.is_a?(self.class) && account == other.account + end + + def to_human + a = if self.alias.present? && self.alias != account + " alias: "+ self.alias + else + "" + end + "<#{print_type} #{account}#{a}>" + end + + def name #nodoc# + self.alias.present? ? self.alias : account + end + +# alias :id :account +end # class + +end # module diff --git a/lib/models/ib/account_value.rb b/models/ib/account_value.rb similarity index 86% rename from lib/models/ib/account_value.rb rename to models/ib/account_value.rb index f3c93f1..a64ed91 100644 --- a/lib/models/ib/account_value.rb +++ b/models/ib/account_value.rb @@ -1,13 +1,13 @@ module IB # Instantiate with a Hash of attributes, to be auto-set via initialize in Model. - class AccountValue < IB::Model + class AccountValue < IB::Base include BaseProperties belongs_to :account prop :key, - :value, - :currency + :value, + :currency # comparison @@ -20,8 +20,8 @@ def == other end def default_attributes super.merge key: 'AccountValue', - value: 0, - currency: 'USD' + value: 0, + currency: 'USD' end def to_human diff --git a/lib/models/ib/bag.rb b/models/ib/bag.rb similarity index 82% rename from lib/models/ib/bag.rb rename to models/ib/bag.rb index 8e54caa..8cf0704 100644 --- a/lib/models/ib/bag.rb +++ b/models/ib/bag.rb @@ -1,13 +1,5 @@ -#require 'models/ib/contract' - module IB - if defined?(Bag) - puts "Bag already a #{defined?(Bag)}" - -# puts Bag.ancestors - IB.send(:remove_const, 'Bag') - end # "BAG" is not really a contract, but a combination (combo) of securities. # AKA basket or bag of securities. Individual securities in combo are represented # by ComboLeg objects. @@ -17,6 +9,9 @@ class Bag < Contract # The exception is for a STK legs, which must specify the SMART exchange. # 2. :symbol => "USD" For combo Contract, this is an arbitrary value (like "USD") + prop :combo_params # bags carry "non_guarantieed: true/false" in combo_params + + validates_format_of :sec_type, :with => /\Abag\z/, :message => "should be a bag" validates_format_of :right, :with => /\Anone\z/, :message => "should be none" validates_format_of :expiry, :with => /\A\z/, :message => "should be blank" @@ -33,14 +28,12 @@ def to_human "" end - def con_id= arg - # dont' update con_id - end + def con_id= arg + # dont' update con_id + end ### Leg-related methods - # TODO: Rewrite with legs and legs_description being strictly in sync... - # TODO: Find a way to serialize legs without references... # IB-equivalent leg description. def legs_description self[:legs_description] || combo_legs.map { |the_leg| "#{the_leg.con_id}|#{the_leg.weight}" }.join(',') @@ -52,6 +45,7 @@ def same_legs? other legs_description.split(',').sort == other.legs_description.split(',').sort end + # Contract comparison def == other super && same_legs?(other) diff --git a/lib/models/ib/bar.rb b/models/ib/bar.rb similarity index 98% rename from lib/models/ib/bar.rb rename to models/ib/bar.rb index 8d02e51..229cd20 100644 --- a/lib/models/ib/bar.rb +++ b/models/ib/bar.rb @@ -1,7 +1,7 @@ module IB # This is a single data point delivered by HistoricData or RealTimeBar messages. # Instantiate with a Hash of attributes, to be auto-set via initialize in Model. - class Bar < IB::Model + class Bar < IB::Base include BaseProperties has_one :contract # The bar represents timeseries info for this Contract diff --git a/lib/models/ib/combo_leg.rb b/models/ib/combo_leg.rb similarity index 96% rename from lib/models/ib/combo_leg.rb rename to models/ib/combo_leg.rb index 390fefa..db62a45 100644 --- a/lib/models/ib/combo_leg.rb +++ b/models/ib/combo_leg.rb @@ -2,7 +2,7 @@ module IB # ComboLeg is essentially a join Model between Combo (BAG) Contract and # individual Contracts (securities) that this BAG contains. - class ComboLeg < IB::Model + class ComboLeg < IB::Base include BaseProperties # BAG Combo Contract that contains this Leg @@ -20,10 +20,10 @@ class ComboLeg < IB::Model # specific combination order, refer to the Interactive # Analytics section of the User's Guide. :exchange, # String: exchange to which the complete combo order will be routed. - # + # # For institutional customers only! For stock legs when doing short sale - :short_sale_slot, # int: 0 - retail(default), - # 1 = clearing broker, 2 = third party + :short_sale_slot, # int: 0 - retail(default), + # 1 = clearing broker, 2 = third party :designated_location, # String: Only for shortSaleSlot == 2. # Otherwise leave blank or orders will be rejected. :exempt_code, # int: (-1) diff --git a/lib/models/ib/contract.rb b/models/ib/contract.rb similarity index 71% rename from lib/models/ib/contract.rb rename to models/ib/contract.rb index 810684d..ff7aec8 100644 --- a/lib/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -1,17 +1,5 @@ -require 'models/ib/contract_detail' -require 'models/ib/underlying' - - - module IB - - if defined?(Contract) - #Connection.current.logger.warn "Contract already a #{defined?(Contract)}" - -# puts Contract.ancestors -# IB.send(:remove_const, 'Contract') - end - class Contract < IB::Model + class Contract < IB::Base include BaseProperties # Fields are Strings unless noted otherwise @@ -38,12 +26,12 @@ class Contract < IB::Model :local_symbol => :s, # Local exchange symbol of the underlying asset :trading_class => :s, # Future/option contract multiplier (only needed when multiple possibilities exist) - :multiplier => {:set => :i}, + :multiplier => :f, :strike => :f, # double: The strike price. :expiry => :s, # The expiration date. Use the format YYYYMM or YYYYMMDD :last_trading_day => :s, # the tws returns the last trading day in Format YYYYMMMDD hh:mm - # which may differ from the expiry + # which may differ from the expiry :exchange => :sup, # The order destination, such as Smart. :primary_exchange => :sup, # Non-SMART exchange where the contract trades. :include_expired => :bool, # When true, contract details requests and historical @@ -76,9 +64,9 @@ class Contract < IB::Model attr_accessor :description # NB: local to ib, not part of TWS. ### Associations - has_many :misc # multi purpose association + has_many :misc # multi purpose association has_many :orders # Placed for this Contract - has_many :portfolio_values + has_many :portfolio_values has_many :bars # Possibly representing trading history for this Contract @@ -125,15 +113,17 @@ def default_attributes # :nodoc: # :exchange => 'SMART', :include_expired => false end + + # This returns an Array of data from the given contract and is used to represent # contracts in outgoing messages. -# +# # Different messages serialize contracts differently. Go figure. -# +# # Note that it does NOT include the combo legs. # serialize :option, :con_id, :include_expired, :sec_id -# -# 18/1/18: serialise always includes conid +# +# 18/1/18: serialise always includes con_id def serialize *fields # :nodoc: print_default = ->(field, default="") { field.blank? ? default : field } @@ -141,11 +131,11 @@ def serialize *fields # :nodoc: print_default[symbol], print_default[self[:sec_type]], ( fields.include?(:option) ? - [ print_default[expiry], - ## a Zero-Strike-Option has to be defined with «strike: -1 » - strike.present? && ( strike.is_a?(Numeric) && !strike.zero? && strike > 0 ) ? strike : strike<0 ? 0 : "", - print_default[self[:right]], - print_default[multiplier]] : nil ), + [ print_default[expiry], + ## a Zero-Strike-Option has to be defined with «strike: -1 » + strike.present? && ( strike.is_a?(Numeric) && !strike.zero? && strike > 0 ) ? strike : strike<0 ? 0 : "", + print_default[self[:right]], + print_default[multiplier]] : nil ), print_default[exchange], ( fields.include?(:primary_exchange) ? print_default[primary_exchange] : nil ) , print_default[currency], @@ -156,41 +146,41 @@ def serialize *fields # :nodoc: ].flatten.compact end - # serialize contract - # con_id. sec_type, expiry, strike, right, multiplier exchange, primary_exchange, currency, local_symbol, include_expired + # serialize contract + # con_id. sec_type, expiry, strike, right, multiplier exchange, primary_exchange, currency, local_symbol, include_expired # other fields on demand def serialize_long *fields # :nodoc: serialize :option, :include_expired, :primary_exchange, :trading_class, *fields end - # serialize contract + # serialize contract # con_id. sec_type, expiry, strike, right, multiplier, exchange, primary_exchange, currency, local_symbol # other fields on demand # acutal used by place_order, request_marketdata, request_market_depth, exercise_options def serialize_short *fields # :nodoc: serialize :option, :trading_class, :primary_exchange, *fields end - - # same as :serialize_short, omitting primary_exchange - # used by RequestMarketDepth + + # same as :serialize_short, omitting primary_exchange + # used by RequestMarketDepth def serialize_supershort *fields # :nodoc: serialize :option, :trading_class, *fields end # Serialize under_comp parameters: EClientSocket.java, line 471 - def serialize_under_comp *args # :nodoc: - under_comp ? under_comp.serialize : [false] + def serialize_under_comp *args # :nodoc: + under_comp ? [true] + under_comp.serialize : [false] end # Defined in Contract, not BAG subclass to keep code DRY - def serialize_legs *fields # :nodoc: + def serialize_legs *fields # :nodoc: case when !bag? - [] + [] when combo_legs.empty? [0] else - [combo_legs.size, combo_legs.map { |the_leg| the_leg.serialize *fields }].flatten + [combo_legs.size, combo_legs.map{|x| x.serialize :extended} ] end end @@ -212,32 +202,33 @@ def serialize_ib_ruby serialize_long.join(":") end - # extracts essential attributes of the contract, - # and returns a new contract. - # - # the link to contract-details is __not__ maintained. - def essential - - the_attributes = [ :sec_type, :symbol , :con_id, :exchange, :right, - :currency, :expiry, :strike, :local_symbol, :last_trading_day, - :multiplier, :primary_exchange, :trading_class, :description ] - new_contract= self.class.new invariant_attributes.select{|k,_| the_attributes.include? k }.compact - new_contract[:description] = if @description.present? - @description + # extracts essential attributes of the contract, + # and returns a new contract. Used for comparism of equality of contracts + # + # the link to contract-details is __not__ maintained. + def essential + + the_attributes = [ :sec_type, :symbol , :con_id, :exchange, :right, + :currency, :expiry, :strike, :local_symbol, :last_trading_day, + :multiplier, :primary_exchange, :trading_class, :description ] + new_contract= self.class.new( invariant_attributes.select{|k,_| the_attributes.include? k } + .transform_values{|v| v.is_a?(Numeric)? v : v.to_s.upcase } ) + new_contract[:description] = if @description.present? + @description elsif contract_detail.present? contract_detail.long_name - else + else "" end new_contract # return contract - end + end - # creates a new Contract substituting attributes by the provided key-value pairs. - # + # creates a new Contract substituting attributes by the provided key-value pairs. + # # for convenience - # con_id, local_symbol and last_trading_day are resetted, - # the link to contract-details is savaged + # con_id, local_symbol and last_trading_day are resetted, + # the link to contract-details is savaged # # Example # ge = Stock.new( symbol: :ge).verify.first @@ -268,65 +259,36 @@ def essential #│ Future │ ES │ 495512572 │ GLOBEX │ 20230317 │ 50 │ ES │ │ │ USD │ #│ Future │ ES │ 497222760 │ GLOBEX │ 20230915 │ 50 │ ES │ │ │ USD │ #└────────┴────────┴───────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘ - + def merge **new_attributes resetted_attributes = [:con_id, :local_symbol, :contract_detail] ## last_trading_day / expiry needs special treatment resetted_attributes << :last_trading_day if new_attributes.keys.include? :expiry self.class.new attributes.reject{|k,_| resetted_attributes.include? k}.merge(new_attributes) - end + end # Contract comparison - def == other # :nodoc: - return false if !other.is_a?(Contract) - return true if super(other) - return true if !con_id.to_i.zero? && con_id == other.con_id - - return false unless other.is_a?(self.class) - - # Different sec_id_type - return false if sec_id_type && other.sec_id_type && sec_id_type != other.sec_id_type - - # Different sec_id - return false if sec_id && other.sec_id && sec_id != other.sec_id - - # Different symbols - return false if symbol && other.symbol && symbol != other.symbol - - # Different currency - return false if currency && other.currency && currency != other.currency - - # Same con_id for all Bags, but unknown for new Contracts... - # 0 or nil con_id matches any - return false if con_id != 0 && other.con_id != 0 && - con_id && other.con_id && con_id != other.con_id - - # SMART or nil exchange matches any - return false if exchange != 'SMART' && other.exchange != 'SMART' && - exchange && other.exchange && exchange != other.exchange - - # Comparison for Bonds and Options - if bond? || option? - return false if right != other.right || strike != other.strike - return false if multiplier && other.multiplier && - multiplier != other.multiplier - return false if expiry && expiry[0..5] != other.expiry[0..5] - return false unless expiry && (expiry[6..7] == other.expiry[6..7] || - expiry[6..7].empty? || other.expiry[6..7].empty?) + def == other # :nodoc: +# a = ->(e){ e.essential.invariant_attributes.select{|y,_| ![:description, :include_expired, :con_id, :trading_class, :primary_exchange].include? y} } + return true if self.con_id == other.con_id +# a.call(self) == a.call(other) + common_keys = self.invariant_attributes.keys & other.invariant_attributes.keys + common_keys.all? do |key| + value1 = attributes[key] + value2 = other.attributes[key] + next true if value1 == value2 + value1.to_i.zero? || value2.to_i.zero? rescue true end - - # All else being equal... - sec_type == other.sec_type end - def to_s - "" - end +# def to_s +# "" +# end def to_human "" end - def to_short - if expiry.blank? && last_trading_day.blank? - "#{symbol}# {exchange}# {currency}" - elsif expiry.present? - "#{symbol}(#{strike}) #{right} #{expiry} /#{exchange}/#{currency}" - else - "#{symbol}(#{strike}) #{right} #{last_trading_day} /#{exchange}/#{currency}" - end - end + alias to_s to_human + # Testing for type of contract: - # depreciated : use is_a?(IB::Stock, IB::Bond, IB::Bag etc) instead - def bag? # :nodoc: + # depreciated : use is_a?(IB::Stock, IB::Bond, IB::Bag etc) instead + def bag? # :nodoc: self[:sec_type] == 'BAG' end @@ -375,10 +330,28 @@ def index? # :nodoc: self[:sec_type] == 'IND' end + def crypto? # :nodoc: + + self[:sec_type] == 'CRYPTO' + end - def verify # :nodoc: - error "verify must be overloaded. Please require at least `ib/verify` from the `ib-extenstions` gem " - end + + def time_zone + if contract_detail.present? + contract_detail.time_zone + else + case currency + when "EUR" + "MET" + when "AUD" + "Australia/NSW" + when "USD" + "US/Eastern" + else + "UTC" + end + end + end =begin From the release notes of TWS 9.50 @@ -392,25 +365,25 @@ def verify # :nodoc: # IB-ruby uses expiry to query Contracts. -# +# # The response from the TWS is stored in 'last_trading_day' (Contract) and 'real_expiration_data' (ContractDetails) -# +# # However, after querying a contract, 'expiry' ist overwritten by 'last_trading_day'. The original 'expiry' # is still available through 'attributes[:expiry]' - def expiry - if self.last_trading_day.present? - last_trading_day.gsub(/-/,'') - else - @attributes[:expiry] - end - end + def expiry + if self.last_trading_day.present? + last_trading_day.gsub(/-/,'') + else + @attributes[:expiry] + end + end + - # is read by Account#PlaceOrder to set requirements for contract-types, as NonGuaranteed for stock-spreads - def order_requirements - Hash.new - end + def order_requirements + Hash.new + end def table_header( &b ) @@ -420,67 +393,19 @@ def table_header( &b ) [ '', 'symbol', 'con_id', 'exchange', 'expiry','multiplier', 'trading-class' , 'right', 'strike', 'currency' ] end end - + def table_row - [ self.class.to_s.demodulize, symbol, - { value: con_id.zero? ? '' : con_id , alignment: :right}, - { value: exchange, alignment: :center}, - expiry, - { value: multiplier.zero?? "" : multiplier, alignment: :center}, - { value: trading_class, alignment: :center}, - {value: right == :none ? "": right, alignment: :center }, - { value: strike.zero? ? "": strike, alignment: :right}, + [ self.class.to_s.demodulize, symbol, + { value: con_id.zero? ? '' : con_id , alignment: :right}, + { value: exchange, alignment: :center}, + expiry, + { value: multiplier.nil? || multiplier.zero?? "" : multiplier, alignment: :center}, + { value: trading_class.nil? ? "" : trading_class, alignment: :center}, + { value: right.nil? || right == :none ? "": right, alignment: :center }, + { value: strike.nil? || strike.zero? ? "": strike, alignment: :right}, { value: currency, alignment: :center} ] end - - - end # class Contract - - - ### Now let's deal with Contract subclasses - begin - - require_relative 'option' - require 'models/ib/bag' - require 'models/ib/forex' - require 'models/ib/future' - require 'models/ib/stock' - require 'models/ib/index' - ### walkaraound to enable spreads with orientdb - if IB::const_defined? :Spread - IB::send(:remove_const, :Spread) - #puts "Spread already defined" - #puts "erasing" - end - require 'models/ib/spread.rb' - end - - - - class Contract - # Contract subclasses representing specialized security types. - using IBSupport - - Subclasses = Hash.new(Contract) - Subclasses[:bag] = IB::Bag - Subclasses[:option] = IB::Option - Subclasses[:futures_option] = IB::FutureOption - Subclasses[:future] = IB::Future - Subclasses[:stock] = IB::Stock - Subclasses[:forex] = IB::Forex - Subclasses[:index] = IB::Index - - - # This builds an appropriate Contract subclass based on its type - # - # the method is also used to copy Contract.values to new instances - def self.build opts = {} - subclass =( VALUES[:sec_type][opts[:sec_type]] || opts['sec_type'] || opts[:sec_type]).to_sym - Contract::Subclasses[subclass].new opts - end - - end # class Contract end # module IB diff --git a/lib/models/ib/contract_detail.rb b/models/ib/contract_detail.rb similarity index 78% rename from lib/models/ib/contract_detail.rb rename to models/ib/contract_detail.rb index 9372cd1..b0e541d 100644 --- a/lib/models/ib/contract_detail.rb +++ b/models/ib/contract_detail.rb @@ -1,7 +1,7 @@ module IB # Additional Contract properties (volatile, therefore extracted) - class ContractDetail < IB::Model + class ContractDetail < IB::Base include BaseProperties # All fields Strings, unless specified otherwise: @@ -18,11 +18,11 @@ class ContractDetail < IB::Model :long_name, # Descriptive name of the asset. :contract_month, # The contract month of the underlying futures contract. - :agg_group, - :under_symbol, - :under_sec_type, - :market_rule_ids, - :real_expiration_date, + :agg_group, + :under_symbol, + :under_sec_type, + :market_rule_ids, + :real_expiration_date, # For Bonds only @@ -68,6 +68,10 @@ class ContractDetail < IB::Model :next_option_date, # only if bond has embedded options. :next_option_type, # only if bond has embedded options. :notes, # Additional notes, if populated for the bond in IB's database + :stock_type, # new Version 10.12 --> common + :min_size, # new Version 10.12 + :size_increment, # new Version 10.12 + :suggested_size_increment, # new Version 10.12 :callable => :bool, # Can be called by the issuer under certain conditions. :puttable => :bool, # Can be sold back to the issuer under certain conditions :convertible => :bool, # Can be converted to stock under certain conditions. @@ -94,21 +98,21 @@ def default_attributes :next_option_partial => false end - def to_human - ret = " #{market_name}, " if market_name.present? - ret << "/C/ #{category}, /I/ #{industry} /SC/ #{subcategory}, " if category.present? - ret << "Underlying:#{under_symbol}[#{under_sec_type}](#{under_con_id}), " unless under_con_id.zero? + def to_human + ret = " #{market_name}, " if market_name.present? + ret << "/C/ #{category}, /I/ #{industry} /SC/ #{subcategory}, " if category.present? + ret << "Underlying:#{under_symbol}[#{under_sec_type}](#{under_con_id}), " unless under_con_id.zero? ret << "ev_multiplier:#{ev_multiplier}, " if ev_multiplier.present? - ret << "convertible:#{convertible}, " if convertible - ret << "coupon:#{coupon}, " if coupon.present? && coupon > 0 - ret << "md_size_multiplier:#{md_size_multiplier}, min_tick:#{min_tick}, " - ret << "next_option_partial:#{next_option_partial}, " if next_option_partial.present? - ret << "price_magnifier:#{price_magnifier}, " - ret << "puttable:#{puttable}, " if puttable.present? - ret << "sec_id-list:#{sec_id_list}, " unless sec_id_list.empty? - ret <<"valid exchanges: #{ valid_exchanges}; order types: #{order_types} >" - end + ret << "convertible:#{convertible}, " if convertible + ret << "coupon:#{coupon}, " if coupon.present? && coupon > 0 + ret << "md_size_multiplier:#{md_size_multiplier}, min_tick:#{min_tick}, " + ret << "next_option_partial:#{next_option_partial}, " if next_option_partial.present? + ret << "price_magnifier:#{price_magnifier}, " + ret << "puttable:#{puttable}, " if puttable.present? + ret << "sec_id-list:#{sec_id_list}, " unless sec_id_list.empty? + ret <<"valid exchanges: #{ valid_exchanges}; order types: #{order_types} >" + end end # class ContractDetail end # module IB diff --git a/lib/models/ib/execution.rb b/models/ib/execution.rb similarity index 98% rename from lib/models/ib/execution.rb rename to models/ib/execution.rb index 4442d0f..fba7ed0 100644 --- a/lib/models/ib/execution.rb +++ b/models/ib/execution.rb @@ -1,7 +1,7 @@ module IB # This is IB Order execution report. - class Execution < IB::Model + class Execution < IB::Base include BaseProperties belongs_to :order diff --git a/lib/models/ib/forex.rb b/models/ib/forex.rb similarity index 82% rename from lib/models/ib/forex.rb rename to models/ib/forex.rb index def7080..1b637ef 100644 --- a/lib/models/ib/forex.rb +++ b/models/ib/forex.rb @@ -1,10 +1,9 @@ -#require 'models/ib/contract' module IB class Forex < IB::Contract validates_format_of :sec_type, :with => /\Aforex\z/, :message => "should be a Currency-Pair" def default_attributes - # Base-currency: USD + # Base-currency: USD super.merge :sec_type => :forex, currency:'USD', exchange:'IDEALPRO' end diff --git a/models/ib/future.rb b/models/ib/future.rb new file mode 100644 index 0000000..490d727 --- /dev/null +++ b/models/ib/future.rb @@ -0,0 +1,64 @@ +module IB + class Future < Contract + validates_format_of :sec_type, :with => /\Afuture\z/, + :message => "should be a Future" + def default_attributes + super.merge :sec_type => :future, currency:'USD' + end + def to_human + "" + end + + + + # get the next (regular) expiry of the contract + # + # fetches for real contracts if verify is available + # + def next_expiry d = Date.today + exp = self.class.next_expiry d + if IB::Connection.current.plugins.include? 'verify' + self.expiry = exp[0..-3] + verify.sort_by{| x | x.last_trading_day} + .find_all{| y | y.expiry <= exp } + .first + else + exp + end + end + class << self + + + # This returns the next + # quarterly expiration month after the current month. + # + # IB::Future.next_expiry returns the next quaterly expiration + # IB::Option.next_expiry returns the next monthly expiration + # + # + # + def next_expiry d=Date.today, type: :quarter + next_quarter_day = ->(year, month) do + base_date = Date.new(year, month) + base_wday = base_date.wday + base_date + ( 5 > base_wday ? 5 - base_wday : 7 - base_wday + 5 ) + 14 + end + next_quarter_day[ next_quarter_year(d), next_quarter_month(d) ].strftime("%Y%m%d") +# /retired/ "#{ next_quarter_year(time) }#{ sprintf("%02d", next_quarter_month(time)) }" + end + + private + # Find the next front month of quarterly futures. + # N.B. This will not work as expected during the front month before expiration, as + # it will point to the next quarter even though the current month is still valid! + def next_quarter_month d + [3, 6, 9, 12].find { |month| month > d.month } || 3 # for December, next March + end + + def next_quarter_year d + next_quarter_month(d) < d.month ? d.year + 1 : d.year + end + end + end + end + diff --git a/lib/models/ib/index.rb b/models/ib/index.rb similarity index 91% rename from lib/models/ib/index.rb rename to models/ib/index.rb index 00fb0c9..bd774b5 100644 --- a/lib/models/ib/index.rb +++ b/models/ib/index.rb @@ -1,4 +1,3 @@ -#require 'models/ib/contract' module IB class Index < Contract validates_format_of :sec_type, :with => /\Aind\z/, diff --git a/models/ib/option.rb b/models/ib/option.rb new file mode 100644 index 0000000..3fa1fef --- /dev/null +++ b/models/ib/option.rb @@ -0,0 +1,149 @@ +module IB + class Option < Contract + + validates_numericality_of :strike, :greater_than => 0 + validates_format_of :sec_type, :with => /\Aoption\z/, + :message => "should be an option" + validates_format_of :local_symbol, :with => /\A\w+\s*\d{6}[pcPC]\d{8}$|\A\z/, + :message => "invalid OSI code" + validates_format_of :right, :with => /\Aput$|^call\z/, + :message => "should be put or call" + + + # introduce Option.greek with reference to IB::OptionDetail-dataset + # + has_one :greek , as: :option_detail + # For Options, this is contract's OSI (Option Symbology Initiative) name/code + alias osi local_symbol + + def osi= value + # Normalize to 21 char + self.local_symbol = value.sub(/ /, ' '*(22-value.size)) + end + + # Make valid IB Contract definition from OSI (Option Symbology Initiative) code. + # NB: Simply making a new Contract with *local_symbol* (osi) property set to a + # valid OSI code works just as well, just do NOT set *expiry*, *right* or + # *strike* properties in this case. + # This class method provided as a backup and shows how to analyse OSI codes. + def self.from_osi osi + + # Parse contract's OSI (OCC Option Symbology Initiative) code + args = osi.match(/(\w+)\s?(\d\d)(\d\d)(\d\d)([pcPC])(\d+)/).to_a.drop(1) + symbol = args.shift + year = 2000 + args.shift.to_i + month = args.shift.to_i + day = args.shift.to_i + right = args.shift.upcase + strike = args.shift.to_i/1000.0 + + # Set correct expiry date - IB expiry date differs from OSI if expiry date + # falls on Saturday (see https://github.com/arvicco/option_mower/issues/4) + expiry_date = Time.utc(year, month, day) + expiry_date = Time.utc(year, month, day-1) if expiry_date.wday == 6 + + new :symbol => symbol, + :exchange => "SMART", + :expiry => expiry_date.to_ib[2..7], # YYMMDD + :right => right, + :strike => strike + end + + def default_attributes + super.merge :sec_type => :option + #self[:description] ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}" + end + def == other + super(other) || ( # finish positive, if contract#== is true + # otherwise, we most probably compare the response from IB with our selfmade input + exchange == other.exchange && + include_expired == other.include_expired && + sec_type == other.sec_type && + multiplier == other.multiplier && + strike == other.strike && + right == other.right && + multiplier == other.multiplier && + expiry == other.expiry ) + + end + + + # returns the verified option for the next (regular) expiry of the contract. + # + # Argument: Reference date, provided as Date-Object or as parseable string or integer, i.e + # Symbols::Options.rut.merge( strike: 2000 ).next_expiry( "2405") + # returns "" instead of + # "" because the third friday is a bank holiday + # + # Optionally a block can be provided, returning the expiry to check in the format "yyyymmdd" + # + # if verify is not available, the option is just returned. + # + # (always returns a new option, respects immutability of the IB::Contract) + # + # raises IB::LoadError if no Option is available for the expiry given. + # + def next_expiry d = Date.today + # get the next regular option + exp = block_given? ? yield : self.class.next_expiry( d ) + # check if the option exists, otherwise fetch the previous date until a valid contract is detected + if IB::Connection.current.plugins.include? 'verify' + real_option = nil + loop do + real_option = merge( expiry: exp ).verify.first + break unless real_option.nil? + exp = ( exp.to_i - 1 ).to_s + error( "No suitable next expiry option found", :load ) if exp[-2..-1] == "00" + end + real_option + else + merge( expiry: exp, last_trading_day: Date.parse( exp ).strftime( "%Y-%m-%d" ) ) + end + + end + + # returns the third friday of the (next) month (class method) + # + # Argument: can either be Date, a String which parses to a Date or + # an Integer, yymm yyyymm or yyyymmdd --> 2406 or 202406 or 20240618 + # + # if called with a digit, this is interpretated a day of the current month + # + def self.next_expiry base = Date.today + + c = 0 + begin + base_date = if base.is_a? Date + [ base.year, base.month ] + else + (base = Date.parse(base.to_s)).then { | d | [ d.year,d.month ] } + end.then{ |y,m| Date.new y,m } + rescue Date::Error => e + base = base.to_s + "01" + c = c + 1 + retry if c == 1 + end + error "Next-Expiry: Not a valid date: #{base}" if base_date.nil? + friday = 5 + base_wday = base_date.wday + b= base_date + ( friday > base_wday ? friday - base_wday : 7 - base_wday + friday ) + 14 + + if b < base + next_expiry base.then{| y | a = y + 25; a.strftime "%Y%m01" } + else + b.strftime "%Y%m%d" + end + end + + def to_human + "" + end + + end # class Option + + class FutureOption < Option + def default_attributes + super.merge :sec_type => :futures_option + end + end +end # module IB diff --git a/lib/models/ib/option_detail.rb b/models/ib/option_detail.rb similarity index 67% rename from lib/models/ib/option_detail.rb rename to models/ib/option_detail.rb index 74cb950..67df94b 100644 --- a/lib/models/ib/option_detail.rb +++ b/models/ib/option_detail.rb @@ -1,68 +1,68 @@ module IB - # Additional Option properties and Option-Calculations - class OptionDetail < IB::Model + class OptionDetail < Base include BaseProperties prop :delta, :gamma, :vega, :theta, # greeks :implied_volatility, - :pv_dividend, # anticipated Dividend - :under_price, # price of the Underlying - :option_price, - :close_price, - :open_tick, - :bid_price, - :ask_price, - :prev_strike, - :next_strike, - :prev_expiry, - :next_expiry, - :option_price, - :updated_at + :pv_dividend, # anticipated Dividend + :under_price, # price of the Underlying + :option_price, + :close_price, + :open_tick, + :bid_price, + :ask_price, + :prev_strike, + :next_strike, + :prev_expiry, + :next_expiry, + :option_price, + :updated_at belongs_to :option # returns true if all datafields are filled with reasonal data def complete? fields= [ :delta, :gamma, :vega, :theta, :implied_volatility, :pv_dividend, :open_tick, - :under_price, :option_price, :close_price, :bid_price, :ask_price] + :under_price, :option_price, :close_price, :bid_price, :ask_price] !fields.detect{|y| self.send(y).nil?} end - def greeks? - fields= [ :delta, :gamma, :vega, :theta, - :implied_volatility] + # true if greeks are present + def greeks? # theta and implied volatility are not always present + fields= [ :delta, :gamma, :vega ] !fields.detect{|y| self.send(y).nil?} end - def prices? - fields = [:implied_volatility, :under_price, :option_price] + # true if prices are received + def prices? + fields = [:implied_volatility, :under_price, :option_price] !fields.detect{|y| self.send(y).nil?} - end + end - def iv - implied_volatility - end + def iv + implied_volatility + end - def spread - bid_price - ask_price - end + def spread + bid_price - ask_price + end def to_human outstr= ->( item ) { if item.nil? then "--" else sprintf("%g" , item) end } att = " optionPrice: #{ outstr[ option_price ]}, UnderlyingPrice: #{ outstr[ under_price] } impl.Vola: #{ outstr[ implied_volatility ]} ; dividend: #{ outstr[ pv_dividend ]}; " greeks = "Greeks:: delta: #{ outstr[ delta ] }; gamma: #{ outstr[ gamma ]}, vega: #{ outstr[ vega ] }; theta: #{ outstr[ theta ]}" prices= " close: #{ outstr[ close_price ]}; bid: #{ outstr[ bid_price ]}; ask: #{ outstr[ ask_price ]} " - if complete? - "< "+ prices + "\n" + att + "\n" + greeks + " >" - elsif prices? - "< " + att + greeks + " >" + if complete? + "< "+ prices + "\n" + att + "\n" + greeks + " >" + elsif prices? + "< " + att + greeks + " >" else - "< " + greeks + " >" + "< " + greeks + " >" end end diff --git a/lib/models/ib/order.rb b/models/ib/order.rb similarity index 53% rename from lib/models/ib/order.rb rename to models/ib/order.rb index df85f3e..77dbdde 100644 --- a/lib/models/ib/order.rb +++ b/models/ib/order.rb @@ -1,7 +1,5 @@ -require 'models/ib/order_state' - module IB - class Order < IB::Model + class Order < IB::Base include BaseProperties # General Notes: @@ -13,9 +11,9 @@ class Order < IB::Model # your own Order IDs to avoid conflicts between orders placed from your API application. # Main order fields - prop :local_id, # int: Order id associated with client (volatile). - :client_id, # int: The id of the client that placed this order. - :perm_id, # int: TWS permanent id, remains the same over TWS sessions. + prop :local_id, # int: Order id associated with client (volatile). + :client_id, # int: The id of the client that placed this order. + :perm_id, # int: TWS permanent id, remains the same over TWS sessions. :quantity, :total_quantity, # int: The order quantity. :order_type, # String: Order type. @@ -30,9 +28,10 @@ class Order < IB::Model :limit_price, # double: LIMIT price, used for limit, stop-limit and relative # orders. In all other cases specify zero. For relative # orders with no limit price, also specify zero. - :aux_price, # => 0.0, default set to "" (as implemented in python code) - #:aux_price, # double: STOP price for stop-limit orders, and the OFFSET amount - # for relative orders. In all other cases, specify zero. + :aux_price, # double: default is set to "" (as implemented in python code) + # STOP price for stop-limit orders, + # OFFSET amount for relative orders. + # In all other cases, specify zero. :oca_group, # String: Identifies a member of a one-cancels-all group. :oca_type, # int: Tells how to handle remaining orders in an OCA group @@ -79,14 +78,14 @@ class Order < IB::Model # IndividualPTIA = 'J', AgencyPTIA = 'U', AgentOtherMemberPTIA = 'M', # IndividualPT = 'K', AgencyPT = 'Y', AgentOtherMemberPT = 'N' :min_quantity, # int: Identifies a minimum quantity order type. - :percent_offset, # double: percent offset amount for relative (REL)orders only - :trail_stop_price, # double: for TRAILLIMIT orders only + :percent_offset, # double: REL-Ordes – percent offset amount + :trail_stop_price, # double: TRAILLIMIT orders only # As of client v.56, we receive trailing_percent in openOrder :trailing_percent, # Financial advisors only - use an empty String if not applicable. :fa_group, :fa_profile, :fa_method, :fa_percentage, - :model_code , # string, no further reference in docs. + :model_code , # string, no further reference in docs. # Institutional orders only! :origin, # 0=Customer, 1=Firm :order_ref, # String: Order reference. Customer defined order ID tag. @@ -107,7 +106,6 @@ class Order < IB::Model # SMART routing only :discretionary_amount, # double: The amount off the limit price # allowed for discretionary orders. - :nbbo_price_cap, # double: Maximum Smart order distance from the NBBO. # BOX or VOL ORDERS ONLY :auction_strategy, # For BOX exchange only. Valid values: @@ -155,14 +153,14 @@ class Order < IB::Model :delta_neutral_settling_firm, :delta_neutral_clearing_account, :delta_neutral_clearing_intent, - # Used when the hedge involves a stock and indicates whether or not it is sold short. - :delta_neutral_short_sale, - # Has a value of 1 (the clearing broker holds shares) or 2 (delivered from a third party). - # If you use 2, then you must specify a deltaNeutralDesignatedLocation. - :delta_neutral_short_sale_slot, - # Specifies whether the order is an Open or a Close order and is used - # when the hedge involves a CFD and and the order is clearing away. - :delta_neutral_open_close, + # Used when the hedge involves a stock and indicates whether or not it is sold short. + :delta_neutral_short_sale, + # Has a value of 1 (the clearing broker holds shares) or 2 (delivered from a third party). + # If you use 2, then you must specify a deltaNeutralDesignatedLocation. + :delta_neutral_short_sale_slot, + # Specifies whether the order is an Open or a Close order and is used + # when the hedge involves a CFD and and the order is clearing away. + :delta_neutral_open_close, # HEDGE ORDERS ONLY: # As of client v.49/50, we can now add hedge orders using the API. @@ -185,7 +183,7 @@ class Order < IB::Model # ALGO ORDERS ONLY: :algo_strategy, # String :algo_params, # public Vector m_algoParams; ?! - :algo_id, # since Vers. 71 + :algo_id, # since Vers. 71 # SCALE ORDERS ONLY: :scale_init_level_size, # int: Size of the first (initial) order component. :scale_subs_level_size, # int: Order size of the subsequent scale order @@ -199,95 +197,108 @@ class Order < IB::Model :scale_profit_offset, :scale_init_position, :scale_init_fill_qty, - :scale_table, # Vers 69 - :active_start_time, # Vers 69 - :active_stop_time, # Vers 69 + :scale_table, # Vers 69 + :active_start_time, # Vers 69 + :active_stop_time, # Vers 69 # pegged to benchmark :reference_contract_id, - :is_pegged_change_amount_decrease, :pegged_change_amount, :reference_change_amount, :reference_exchange_id , - :conditions, # Conditions determining when the order will be activated or canceled. + :conditions, # Conditions determining when the order will be activated or canceled. ### http://xavierib.github.io/twsapidocs/order_conditions.html :conditions_ignore_rth, # bool: Indicates whether or not conditions will also be valid outside Regular Trading Hours :conditions_cancel_order,# bool: Conditions can determine if an order should become active or canceled. + #AdjustedOrderParams :adjusted_order_type, :trigger_price, + :trail_stop_price, :limit_price_offset, # used in trailing stop limit + trailing limit orders :adjusted_stop_price, :adjusted_stop_limit_price, :adjusted_trailing_amount, - :adjustable_trailing_unit, :ext_operator , # 105: MIN_SERVER_VER_EXT_OPERATOR - # This is a regulartory attribute that applies - # to all US Commodity (Futures) Exchanges, provided - # to allow client to comply with CFTC Tag 50 Rules. - :soft_dollar_tier_name, # 106: MIN_SERVER_VER_SOFT_DOLLAR_TIER - :soft_dollar_tier_value, - :soft_dollar_tier_display_name, - # Define the Soft Dollar Tier used for the order. - # Only provided for registered professional advisors and hedge and mutual funds. - # format: "#{name}=#{value},#{display_name}", name and value are used in the - # order-specification. Its included as ["#{name}","#{value}"] pair - - :cash_qty, # 111: MIN_SERVER_VER_CASH_QTY - # decimal : The native cash quantity - :mifid_2_decision_maker, - :mifid_2_decision_algo, - :mifid_2_execution_maker, - :mifid_2_execution_algo, - :dont_use_auto_price_for_hedge, - :discretionary_up_to_limit_price + # This is a regulartory attribute that applies + # to all US Commodity (Futures) Exchanges, provided + # to allow client to comply with CFTC Tag 50 Rules. + :soft_dollar_tier_name, # 106: MIN_SERVER_VER_SOFT_DOLLAR_TIER + :soft_dollar_tier_value, + :soft_dollar_tier_display_name, + # Define the Soft Dollar Tier used for the order. + # Only provided for registered professional advisors and hedge and mutual funds. + # format: "#{name}=#{value},#{display_name}", name and value are used in the + # order-specification. Its included as ["#{name}","#{value}"] pair + + :cash_qty, # decimal : The native cash quantity + :mifid_2_decision_maker, + :mifid_2_decision_algo, + :mifid_2_execution_maker, + :mifid_2_execution_algo, + :dont_use_auto_price_for_hedge,# => :bool, + :discretionary_up_to_limit_price,# => :bool, + :use_price_management_algo,# => :bool, + :duration ,# => :int, + :post_to_ats ,# => :int, + :auto_cancel_parent, # => :bool + :is_O_ms_container, + :advanced_order_reject, + :manual_order_time, + :min_trade_qty, + :min_compete_size, + :compete_against_best_offset, + :mid_offset_at_whole, + :mid_offset_at_half, + :customer_account, + :professional_account + + # Properties with complex processing logics prop :tif, # String: Time in Force (time to market): DAY/GAT/GTD/GTC/IOC - :random_size => :bool, # Vers 76 - :random_price => :bool, # Vers 76 + :random_size => :bool, + :random_price => :bool, :scale_auto_reset => :bool, :scale_random_percent => :bool, - :solicided => :bool, # Vers 73 - :what_if => :bool, # Only return pre-trade commissions and margin info, do not place - :not_held => :bool, # Not Held - :outside_rth => :bool, # Order may trigger or fill outside of regular hours. (WAS: ignore_rth) - :hidden => :bool, # Order will not be visible in market depth. ISLAND only. - :transmit => :bool, # If false, order will be created but not transmitted. - :block_order => :bool, # This is an ISE Block order. - :sweep_to_fill => :bool, # This is a Sweep-to-Fill order. + :solicided => :bool, + :what_if => :bool, # Only return pre-trade commissions and margin info, do not place + :not_held => :bool, # Not Held + :outside_rth => :bool, # Order may trigger or fill outside of regular hours. (WAS: ignore_rth) + :hidden => :bool, # Order will not be visible in market depth. ISLAND only. + :transmit => :bool, # If false, order will be created but not transmitted. + :block_order => :bool, # This is an ISE Block order. + :sweep_to_fill => :bool, # This is a Sweep-to-Fill order. :override_percentage_constraints => :bool, # TWS Presets page constraints ensure that your price and size order values # are reasonable. Orders sent from the API are also validated against these # safety constraints, unless this parameter is set to True. - :all_or_none => :bool, # AON - :etrade_only => :bool, # Trade with electronic quotes. - :firm_quote_only => :bool, # Trade with firm quotes. - :opt_out_smart_routing => :bool, # Australian exchange only, default false + :all_or_none => :bool, # AON + :opt_out_smart_routing => :bool, # Australian exchange only, default false + :is_pegged_change_amount_decrease => :bool, # pegged_to_benchmark-oders, default false (increase) :open_close => PROPS[:open_close], # Originally String: O=Open, C=Close () # for ComboLeg compatibility: SAME = 0; OPEN = 1; CLOSE = 2; UNKNOWN = 3; - [:side, :action] => PROPS[:side], # String: Action/side: BUY/SELL/SSHORT/SSHORTX - :is_O_ms_container => :bool + [:side, :action] => PROPS[:side] # String: Action/side: BUY/SELL/SSHORT/SSHORTX prop :placed_at, :modified_at, :leg_prices, :algo_params, - :combo_params # Valid tags are LeginPrio, MaxSegSize, DontLeginNext, ChangeToMktTime1, - # ChangeToMktTime2, ChangeToMktOffset, DiscretionaryPct, NonGuaranteed, - # CondPriceMin, CondPriceMax, and PriceCondConid. - # to set an execuction-range of a security: - # PriceCondConid, 10375; -- conid of the combo-leg - # CondPriceMax, 62.0; -- max and min-price - # CondPriceMin.;60.0 - - + :combo_params # Valid tags are LeginPrio, MaxSegSize, DontLeginNext, ChangeToMktTime1, + # ChangeToMktTime2, ChangeToMktOffset, DiscretionaryPct, NonGuaranteed, + # CondPriceMin, CondPriceMax, and PriceCondConid. + # to set an execuction-range of a security: + # PriceCondConid, 10375; -- conid of the combo-leg + # CondPriceMax, 62.0; -- max and min-price + # CondPriceMin.;60.0 + + prop :etrade_only, :firm_quote_only, :nbbo_price_cap # depreciated, needed for open-order message # prop :misc1, :misc2, :misc3, :misc4, :misc5, :misc6, :misc7, :misc8 # just 4 debugging alias order_combo_legs leg_prices alias smart_combo_routing_params combo_params - # serialize is included for active_record compatibility + # serialize is included for active_record compatibility # serialize :leg_prices # serialize :conditions # serialize :algo_params, Hash @@ -295,15 +306,15 @@ class Order < IB::Model # serialize :soft_dollar_tier_params, HashWithIndifferentAccess serialize :mics_options, Hash - # Order is always placed for a contract. Here, we explicitly set this link. + # Order is always placed for a contract. We explicitly set this link. belongs_to :contract # Order has a collection of Executions if it was filled has_many :executions - # Order has a collection of OrderStates, last one is always current + # Order has a collection of OrderStates. The last one is always current has_many :order_states - # Order can have multible conditions + # Order can have multible conditions has_many :conditions def order_state @@ -327,9 +338,6 @@ def order_state= state :min_commission, # The possible min range of the actual order commission. :max_commission, # The possible max range of the actual order commission. :warning_text, # String: Displays a warning message if warranted. - :init_margin, # Float: The impact the order would have on your initial margin. - :maint_margin, # Float: The impact the order would have on your maintenance margin. - :equity_with_loan, # Float: The impact the order would have on your equity :status, # String: Displays the order status. See OrderState for values # Properties arriving via OrderStatus message: :filled, # int @@ -348,6 +356,11 @@ def order_state= state :complete_fill? ].each { |property| define_method(property) { order_state.send(property) } } + [:init_margin, # Float: The impact the order would have on your initial margin. + :equity_with_loan, # Float: The impact the order would have on your equity + :maint_margin # Float: The impact the order would have on your maintenance margin. + ].each { |property| define_method(property) { order_state.send(property.to_s+"_change") } } + # Order is not valid without correct :local_id validates_numericality_of :local_id, :perm_id, :client_id, :parent_id, :total_quantity, :min_quantity, :display_size, @@ -356,124 +369,269 @@ def order_state= state validates_numericality_of :limit_price, :aux_price, :allow_nil => true - def default_attributes # default valus are taken from order.java - # public Order() { } + def default_attributes # default valus are taken from order.java + # public Order() { } super.merge( - :active_start_time => "", # order.java # 470 # Vers 69 - :active_stop_time => "", #order.java # 471 # Vers 69 + :active_start_time => "", # order.java # 470 # Vers 69 + :active_stop_time => "", # order.java # 471 # Vers 69 + :adjusted_order_type => "", + :algo_params => Hash.new, #{}, :algo_strategy => '', - :algo_id => '' , # order.java # 495 - :auction_strategy => :none, - :conditions => [], + :algo_id => '' , # order.java # 495 + :all_or_none => false, + :auction_strategy => :none, + :aux_price => server_version < KNOWN_SERVERS[ :min_server_ver_trailing_percent ] ? 0 : '', + :block_order => false, + :combo_params => Hash.new, + :conditions => [], :continuous_update => 0, + :delta => "", :designated_location => '', # order.java # 487 - :display_size => 0, + :display_size => nil, :discretionary_amount => 0, - :etrade_only => true, # stolen from python client :exempt_code => -1, - :ext_operator => '' , # order.java # 499 - :firm_quote_only => true, # stolen from python client - :not_held => false, # order.java # 494 + :ext_operator => '' , # order.java # 499 + :hedge_param => [], + :hidden => false, + :is_pegged_change_amount_decrease => false, + :leg_prices => [], + :limit_price => server_version < KNOWN_SERVERS[ :min_server_ver_order_combo_legs_price ] ? 0 : '', + :min_quantity => "", + :model_code => "", + :not_held => false, # order.java # 494 :oca_type => :none, - :order_type => :limit, - :open_close => :open, # order.java # - :opt_out_smart_routing => false, + :order_type => :limit, + :open_close => :open, # order.java # + :opt_out_smart_routing => false, + :order_state => IB::OrderState.new( :status => 'New', + :filled => 0, + :remaining => 0, + :price => 0, + :average_price => 0 ), :origin => :customer, - :outside_rth => false, # order.java # 472 + :outside_rth => false, # order.java # 472 + :override_percentage_constraints => false, + :percent_offset =>"", :parent_id => 0, - :random_size => false, #oder.java 497 # Vers 76 - :random_price => false, # order.java # 498 # Vers 76 - :scale_auto_reset => false, # order.java # 490 - :scale_random_percent => false, # order.java # 491 - :scale_table => "", # order.java # 492 + :pegged_change_amount => 0.0, + :random_size => false, #oder.java 497 # Vers 76 + :random_price => false, # order.java # 498 # Vers 76 + :reference_price_type => "", + :reference_contract_id => 0, + :reference_change_amount => 0.0, + :reference_exchange_id => "", + :scale_auto_reset => false, # order.java # 490 + :scale_random_percent => false, # order.java # 491 + :scale_table => "", # order.java # 492 + :stock_range_lower => "", + :stock_range_upper => "", + :stock_ref_price =>"", :short_sale_slot => :default, :solicided => false, # order.java # 496 + :sweep_to_fill => false, :tif => :day, + :trail_stop_price => "", + :trailing_percent => "", :transmit => true, - :trigger_method => :default, + :trigger_method => :default, + :use_price_management_algo => "", + :volatility_type => :annual, :what_if => false, # order.java # 493 - :leg_prices => [], - :algo_params => Hash.new, #{}, - :combo_params =>[], #{}, - # :soft_dollar_tier_params => HashWithIndifferentAccess.new( - # :name => "", - # :val => "", - # :display_name => ''), - :order_state => IB::OrderState.new(:status => 'New', - :filled => 0, - :remaining => 0, - :price => 0, - :average_price => 0) + ) # closing of merge end + def serialize_combo_legs(contract) + if contract.bag? + [ contract.serialize_legs, + # todo implement usecase + leg_prices.empty? ? 0 : [ leg_prices.size, leg_prices ], + # combo_params is an array of hashes + contract.combo_params.nil? || contract.combo_params.empty? ? 0 : [ contract.combo_params.size, contract.combo_params.to_a ] + ] + else + [] + end + end + def serialize_main_order_fields + include_short = -> (s) { if s == :short then 'SSHORT' else s == :short_exempt ? 'SSHORTX' : s.to_sup end } + include_total_quantity = -> (q) { server_version >= KNOWN_SERVERS[ :min_server_ver_fractional_positions ] ? q.to_d : q.to_i } + + [ include_short[ side ], + include_total_quantity[ total_quantity ], + self[ :order_type ], # Internal code, 'LMT' instead of :limit + limit_price , + aux_price ] + end + + def serialize_extended_order_fields + + [ self[ :tif ], + oca_group, + account, + open_close.to_sup[0], # "O" or "C" + self[ :origin ], # translates :customer, :firm to 0,1 + order_ref, + transmit, + parent_id, + block_order, + sweep_to_fill, + display_size, + self[ :trigger_method ], + outside_rth, + hidden ] + end + + def serialize_auxilery_order_fields + [ "", # deprecated shares_allocation field + discretionary_amount, + good_after_time, + good_till_date, + serialize_advisory_order_fields + ] + end =begin rdoc Format of serialisation - count of records - for each condition: conditiontype, condition-fields + count of records + for each condition: conditiontype, condition-fields =end - def serialize_conditions - if conditions.empty? - 0 - else - [ conditions.count ] + conditions.map( &:serialize )+ [ conditions_ignore_rth, conditions_cancel_order] - end - end - - def serialize_algo - if algo_strategy.nil? || algo_strategy.empty? - [algo_strategy, algo_id] # just omit the size and content-field + def serialize_conditions + if conditions.empty? + [ 0 ] else - [algo_strategy, - algo_params.size, - algo_params.to_a, - algo_id ] # Vers 71 + [ conditions.size ] + conditions.map( &:serialize ) + [ conditions_ignore_rth, conditions_cancel_order ] end end - # def serialize_soft_dollar_tier - # [soft_dollar_tier_params[:name],soft_dollar_tier_params[:val]] - # end + def serialize_algo + return [''] if algo_strategy.blank? + [algo_strategy, algo_params.size] + algo_params.to_a + end - # def initialize_soft_dollar_tier *fields - # self.soft_dollar_tier_params= HashWithIndifferentAccess.new( - # name: fields.pop, val: fields.pop, display_name: fields.pop ) - # end + def serialize_advisory_order_fields + aof = [ fa_group, fa_method, fa_percentage, fa_profile ] + if server_version < KNOWN_SERVERS[:min_server_ver_fa_profile_desupport] + aof + else + aof[ 0..-2 ] + end + end - def serialize_misc_options - "" # Vers. 70 + def serialize_volatility_order_fields + if volatility.present? + [ volatility , # Volatility orders + self[:volatility_type] ] # default: annual volatility + else + ["",""] + end + end + + def serialize_delta_neutral_order_fields + + if delta_neutral_order_type && delta_neutral_order_type != :none + [ + delta_neutral_con_id, + delta_neutral_settling_firm, + delta_neutral_clearing_account, + self[ :delta_neutral_clearing_intent ], + delta_neutral_open_close, + delta_neutral_short_sale, + delta_neutral_short_sale_slot, + delta_neutral_designated_location + ] + else + ['', ''] + end + end + + def serialize_scale_order_fields + + a= [ scale_init_level_size || "", + scale_subs_level_size || "", + scale_price_increment || "" ] + + # Support for extended scale orders parameters + if scale_price_increment.to_i > 0 + a << [ scale_price_adjust_value || "", + scale_price_adjust_interval || "", + scale_profit_offset || "", + scale_auto_reset, # default: false, + scale_init_position || "", + scale_init_fill_qty || "", + scale_random_percent # default: false, + ] + end + + a << scale_table + a << active_start_time || "" + a << active_stop_time || "" + a end - # Placement - # - # The Order is only placed, if local_id is not set - # - # Modifies the Order-Object and returns the assigned local_id - def place the_contract=nil, connection=nil - connection ||= IB::Connection.current - error "Unable to place order, next_local_id not known" unless connection.next_local_id - error "local_id present. Order is already placed. Do you want to modify?" unless local_id.nil? - self.client_id = connection.client_id - self.local_id = connection.next_local_id - connection.next_local_id += 1 - self.placed_at = Time.now - modify the_contract, connection, self.placed_at + def serialize_pegged_order_fields + if order_type == :pegged_to_benchmark && server_version >= KNOWN_SERVERS[ :min_server_ver_pegged_to_benchmark ] + [ reference_contract_id, + is_pegged_change_amount_decrease, + pegged_change_amount, + reference_change_amount, + reference_exchange_id ] + else + [] + end + end + + def serialize_advanced_option_order_fields + + [ starting_price , # pegged to stock + stock_ref_price , # pegged to stock + delta , # pegged to stock + stock_range_lower , # pegged + stock_range_upper # pegged + ] end - # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns local_id. - def modify the_contract=nil, connection=nil, time=Time.now - error "Unable to modify order; local_id not specified" if local_id.nil? - self.contract = the_contract unless the_contract.nil? - connection ||= IB::Connection.current - self.modified_at = time - connection.send_message :PlaceOrder, - :order => self, - :contract => contract, - :local_id => local_id - local_id + def serialize_soft_dollar_tier + [ soft_dollar_tier_name, + soft_dollar_tier_value + ] end + + def serialize_mifid_order_fields + a = [] + if server_version >= KNOWN_SERVERS[:min_server_ver_decision_maker] # 138 + a << [ mifid_2_decision_maker, mifid_2_decision_algo ] + end + if server_version >= KNOWN_SERVERS[:min_server_ver_mifid_execution] # 139 + a << [ mifid_2_execution_maker, mifid_2_execution_algo ] + end + a + end + + def serialize_peg_best_and_mid + return [] unless server_version >= KNOWN_SERVERS[:min_server_ver_pegbest_pegmid_offsets] + a = [] + send_mid_offsets = false + a << min_trade_qty if contract.exchange == 'IBKRATS' + if order.type == :pegged_to_best + a << min_compete_size + a << compete_against_best_offset + send_mid_offsets = true if compete_against_best_offset.nil? # TODO: float max? + end + if order.type == :pegged_to_midpoint + send_mid_offsets = true + end + if send_mid_offsets + a << mid_offset_at_whole + a << mid_offset_at_half + end + a + end + + def serialize_misc_options + "" # Vers. 70 + end # Order comparison def == other super(other) || @@ -506,36 +664,51 @@ def to_s #human end def to_human + misc = [] + misc << algo_strategy if algo_strategy.present? + misc << "benchmark con-id: #{reference_contract_id}" if reference_contract_id.to_i >0 + misc << "vola: #{volatility}" if volatility.present? + misc << "fee: #{commission}" if commission.present? + misc << "dc: #{discretionary_amount}," if discretionary_amount.to_i != 0 "" + (misc.empty? ? "" : " ") + misc.join( " " ) + ">" end + alias inspect to_human + def table_header - [ 'account','status' ,'', 'Type', 'tif', 'action', 'amount','price' , 'id/fee' ] + [ 'account','status', 'contract', 'type', 'tif', 'action', 'amount','price' , '','misc' ] end def table_row - [ account, order_ref.present? ? order_ref.to_s : status, - contract.to_human[1..-2], + misc = [] + misc << algo_strategy if algo_strategy.present? + misc << " benchmark con-id: #{reference_contract_id}" if reference_contract_id.to_i > 0 + misc << " vola: #{volatility}" if volatility.present? + misc << " fee: #{commission}" if commission.present? + misc << " id: #{local_id}" if local_id.to_i > 0 + misc << " dc: #{discretionary_amount}," if discretionary_amount.to_i != 0 + [ account, order_ref.present? ? order_ref.to_s : status, + contract.to_human.then{ |y| y.size > 20 ? y[ 1 .. 20 ] + "…" : y[1..-2] }, self[:order_type] , self[:tif], action, total_quantity, - (limit_price ? "#{limit_price} " : '') + ((aux_price && aux_price != 0) ? "/#{aux_price}" : '') , - commission ? " fee #{commission}" : local_id ] + ((limit_price && !limit_price.to_i.zero?) ? "#{limit_price} " : '') + ((aux_price && !aux_price.to_i.zero?) ? "/#{aux_price}" : '') , !what_if.blank? ? "Preview" : "", + misc.join( " " ) ] end - def serialize_rabbit - { 'Contract' => contract.present? ? contract.serialize( :option, :trading_class ): '' , - 'Order' => self, - 'OrderState' => order_state} - end + def serialize_rabbit + { 'Contract' => contract.present? ? contract.serialize( :option, :trading_class ): '' , + 'Order' => self, + 'OrderState' => order_state} + end end # class Order end # module IB diff --git a/lib/models/ib/order_state.rb b/models/ib/order_state.rb similarity index 81% rename from lib/models/ib/order_state.rb rename to models/ib/order_state.rb index cb9fadc..e41c875 100644 --- a/lib/models/ib/order_state.rb +++ b/models/ib/order_state.rb @@ -2,22 +2,25 @@ module IB # OrderState represents dynamic (changeable) info about a single Order, # isolating these changes and making Order essentially immutable - class OrderState < IB::Model + class OrderState < IB::Base include BaseProperties #p column_names belongs_to :order # Properties arriving via OpenOrder message - prop :init_margin, # Float: The impact the order would have on your initial margin. - :maint_margin, # Float: The impact the order would have on your maintenance margin. - :equity_with_loan, # Float: The impact the order would have on your equity + prop :init_margin_after, # Float: The impact the order would have on your initial margin. + :maint_margin_after, # Float: The impact the order would have on your maintenance margin. + :equity_with_loan_after, # Float: The impact the order would have on your equity + :init_margin_before, :maint_margin_before, :equity_with_loan_before, + :init_margin_change, :maint_margin_change, :equity_with_loan_change, :commission, # double: Shows the commission amount on the order. :min_commission, # The possible min range of the actual order commission. :max_commission, # The possible max range of the actual order commission. + :commission_currency, # String: Shows the currency of the commission. :warning_text, # String: Displays a warning message if warranted. - + :market_cap_price # messages#incomming#orderstae#vers. 11 # Properties arriving via OrderStatus message: @@ -34,7 +37,7 @@ class OrderState < IB::Model :parent_id, # int: The order ID of the parent (original) order, used :status => :s # String: one of # ApiCancelled, PreSubmitted, PendingCancel, Cancelled, Submitted, Filled, - # Inactive, PendingSubmit, Unknown, ApiPending, + # Inactive, PendingSubmit, Unknown, ApiPending, # # Displays the order status. Possible values include: # - PendingSubmit - indicates that you have transmitted the order, but @@ -71,8 +74,8 @@ class OrderState < IB::Model def self.valid_status? the_message valid_stati = %w( ApiCancelled PreSubmitted PendingCancel Cancelled Submitted Filled - Inactive PendingSubmit Unknown ApiPending) - valid_stati.include?( the_message ) + Inactive PendingSubmit Unknown ApiPending) + valid_stati.include?( the_message ) end ## Testing Order state: @@ -116,9 +119,9 @@ def == other filled == other.filled && remaining == other.remaining && last_fill_price == other.last_fill_price && - init_margin == other.init_margin && - maint_margin == other.maint_margin && - equity_with_loan == other.equity_with_loan && + init_margin_after == other.init_margin_after && + maint_margin_after == other.maint_margin_after && + equity_with_loan_after == other.equity_with_loan_after && why_held == other.why_held && warning_text == other.warning_text && commission == other.commission @@ -128,8 +131,8 @@ def to_human " 0 ? " fee #{commission}" : "") + (why_held ? " why_held #{why_held}" : '') + ((warning_text && warning_text != '') ? " warning #{warning_text}" : '') + ">" @@ -137,16 +140,16 @@ def to_human alias to_s to_human =begin -If an Order is submitted with the :what_if-Flag set, commission and margin are returned +If an Order is submitted with the :what_if-Flag set, commission and margin are returned via the order_state-Object. =end - def forcast - { :init_margin => init_margin, - :maint_margin => maint_margin, - :equity_with_loan => equity_with_loan , - :commission => commission, - :commission_currency=> commission_currency, - :warning => warning_text } - end + def forcast + { :init_margin => init_margin_after, + :maint_margin => maint_margin_after, + :equity_with_loan => equity_with_loan_after , + :commission => commission, + :commission_currency=> commission_currency, + :warning => warning_text } + end end # class Order end # module IB diff --git a/lib/models/ib/portfolio_value.rb b/models/ib/portfolio_value.rb similarity index 54% rename from lib/models/ib/portfolio_value.rb rename to models/ib/portfolio_value.rb index d967c19..91fd3dc 100644 --- a/lib/models/ib/portfolio_value.rb +++ b/models/ib/portfolio_value.rb @@ -1,18 +1,18 @@ module IB -class PortfolioValue < IB::Model +class PortfolioValue < IB::Base include BaseProperties -# belongs_to :currency - belongs_to :account - belongs_to :contract +# belongs_to :currency + belongs_to :account + belongs_to :contract -# scope :single, ->(key) { where :schluessel => key } rescue nil +# scope :single, ->(key) { where :schluessel => key } rescue nil prop :position, - :market_price, - :market_value, - :average_cost, - :unrealized_pnl, - :realized_pnl + :market_price, + :market_value, + :average_cost, + :unrealized_pnl, + :realized_pnl # Order comparison @@ -22,28 +22,28 @@ def == other market_price == other.market_price && average_cost == other.average_cost && position == other.position && - unrealized_pnl == other.unrealized_pnl && - realized_pnl == other.realized_pnl && + unrealized_pnl == other.unrealized_pnl && + realized_pnl == other.realized_pnl && contract == other.contract end def to_human - the_account = if account.present? - if account.is_a?(String) - account + " " - else - account.account+" " - end - else - "" - end + the_account = if account.present? + if account.is_a?(String) + account + " " + else + account.account+" " + end + else + "" + end - "" ) + - contract.to_human + contract.to_human end alias to_s to_human @@ -58,15 +58,15 @@ def table_header def table_row outprice= ->( item ) { { value: item.nil? ? "--" : item , alignment: :right } } - the_account = if account.present? - if account.is_a?(String) - account + " " - else - account.account+" " - end - else - "" - end + the_account = if account.present? + if account.is_a?(String) + account + " " + else + account.account+" " + end + else + "" + end entry = average_cost.to_f / (contract.multiplier.to_i.zero? ? 1 : contract.multiplier.to_i) @@ -76,7 +76,7 @@ def table_row outprice[entry.to_f.round(3)], outprice[market_price.to_f.round(3)], outprice[market_value.to_f.round(2)], - unrealized_pnl.to_i.zero? ? "": outprice[unrealized_pnl], + unrealized_pnl.to_i.zero? ? "": outprice[unrealized_pnl], realized_pnl.to_i.zero? ? "" : outprice[realized_pnl] ] end diff --git a/models/ib/spread.rb b/models/ib/spread.rb new file mode 100644 index 0000000..3a9cbed --- /dev/null +++ b/models/ib/spread.rb @@ -0,0 +1,176 @@ +module IB + class Spread < Bag + has_many :legs + + using IB::Support + +=begin +Parameters: front: YYYMM(DD) + back: {n}w, {n}d or YYYYMM(DD) + +Adds (or substracts) relative (back) measures to the front month, just passes absolute YYYYMM(DD) value + + front: 201809 back: 2m (-1m) --> 201811 (201808) + front: 20180908 back: 1w (-1w) --> 20180918 (20180902) +=end + + def self.transform_distance front, back + # Check Format of back: 201809 --> > 200.000 + # 20180989 ---> 20.000.000 + start_date = front.to_i < 20000000 ? Date.strptime(front.to_s,"%Y%m") : Date.strptime(front.to_s,"%Y%m%d") + nb = if back.to_i > 200000 + back.to_i + elsif back[-1] == "w" && front.to_i > 20000000 + start_date + (back.to_i * 7) + 1 # +1 to compensate for friday's bank-holiday, target has to be verified through next_expiry + elsif back[-1] == "m" && front.to_i > 200000 + start_date >> back.to_i + else + error "Wrong date #{back} required format YYYMM, YYYYMMDD ord {n}w or {n}m" + end + if nb.is_a?(Date) + if back[-1]=='w' + nb.strftime("%Y%m%d") + else + nb.strftime("%Y%m") + end + else + nb + end + end # def + + def to_human + self.description + end + + def calculate_spread_value( array_of_portfolio_values ) + array_of_portfolio_values.map{|x| x.send yield }.sum if block_given? + end + + def fake_portfolio_position( array_of_portfolio_values ) + calculate_spread_value= ->( a_o_p_v, attribute ) do + a_o_p_v.map{|x| x.send attribute }.sum + end + ar=array_of_portfolio_values + IB::PortfolioValue.new contract: self, + average_cost: calculate_spread_value[ar, :average_cost], + market_price: calculate_spread_value[ar, :market_price], + market_value: calculate_spread_value[ar, :market_value], + unrealized_pnl: calculate_spread_value[ar, :unrealized_pnl], + realized_pnl: calculate_spread_value[ar, :realized_pnl], + position: 0 + + end + + + # adds a leg to any spread + # + # Parameter: + # contract: Will be verified. Contract.essential is added to legs-array + # action: :buy or :sell + # weight: + # ratio: + # + # Default: action: :buy, weight: 1 + + def add_leg contract, **leg_params + error "need a IB::Contract as first argument" unless contract.is_a? IB::Contract + self.legs << contract + error "cannot add leg if no con_id is provided" if contract.con_id.blank? + # weigth = 1 --> sets Combo.side to buy and overwrites the action statement +# leg_params[:weight] = 1 unless leg_params.key?(:weight) || leg_params.key?(:ratio) + leg_description = leg_params.extract!( :description ) + leg_description = "#{leg_params[:action] || 'buy'} #{leg_params[:weight] || "1"} #{contract.to_human}" if leg_description.empty? + self.combo_legs << ComboLeg.new( contract.attributes.slice( :con_id, :exchange ).merge( leg_params )) + self.description = "#{description.nil? ? "": description + " / "} #{leg_description}" rescue "Spread: #{contract.to_human}" + + self # return object to enable chaining + + + end + + # removes the contract from the spread definition + # + def remove_leg contract_or_position = nil + contract = if contract_or_position.is_a? (IB::Contract) + contract_or_position + elsif contract_or_position.is_a? Numeric + legs.at contract_or_position + else + error "Specify a contract to be removed or the position in the legs-array as parameter to remove a leg" + end + the_con_id = contract.verify.first &.con_id + error "Invalid Contract specified" unless the_con_id.is_a? Numeric + legs.delete_if { |x| x.con_id == the_con_id } + combo_legs.delete_if { |x| x.con_id == the_con_id } + self.description = description + " removed #{contract.to_human}" + self # make method chainable + end + +# essentail +# effectivley clones the object +# + def essential + the_es = self.class.new invariant_attributes + the_es.legs = legs.map{|y| IB::Contract.build y.invariant_attributes} + the_es.combo_legs = combo_legs.map{|y| IB::ComboLeg.new y.invariant_attributes } + the_es.description = description + the_es # return + end + + def multiplier + (legs.map(&:multiplier).sum/legs.size).to_i + end + + # provide a negative con_id + def con_id + if attributes[:con_id].present? && attributes[] < 0 + attributes[:con_id] + else + -legs.map{ |x| x.is_a?(String) ? x.expand.con_id : x.con_id}.sum + end + end + + +# def non_guaranteed= x +# super.merge combo_params: [ ['NonGuaranteed', x] ] +# end +# +# +# def non_guaranteed +# combo_params['NonGuaranteed'] +# end +# optional: specify default order prarmeters for all spreads +# def order_requirements +# super.merge symbol: symbol +# end + + + def as_table + t= Terminal::Table.new title: description[1..-2] , + headings: table_header, + + style: { border: :unicode } + + t.add_row table_row + legs.each{ |y| t.add_row y.table_row } + t.render + + end + + def self.build_from_json container + read_leg = ->(a) do + IB::ComboLeg.new :con_id => a.read_int, + :ratio => a.read_int, + :action => a.read_string, + :exchange => a.read_string + + end + object= self.new container['Spread'].clone.read_contract + object.legs = container['legs'].map{|x| IB::Contract.build x.clone.read_contract} + object.combo_legs = container['combo_legs'].map{ |x| read_leg[ x.clone ] } + object.description = container['misc'].clone.read_string + object + + end + end +end diff --git a/models/ib/stock.rb b/models/ib/stock.rb new file mode 100644 index 0000000..297d31d --- /dev/null +++ b/models/ib/stock.rb @@ -0,0 +1,25 @@ +module IB + class Stock < IB::Contract + validates_format_of :sec_type, :with => /\Astock\z/, + :message => "should be a Stock" + validates_format_of :symbol, with: /\A.*\z/, + message: 'should not be blank' + def default_attributes + super.merge :sec_type => :stock, currency:'USD', exchange:'SMART' + end + + def merge **new_attributes + super( **{ trading_class: '', primary_exchange: '' }.merge(new_attributes) ) + end + + def to_human + att = [ symbol, + currency, ( exchange == 'SMART' ? nil: exchange ), + (primary_exchange.present? && !primary_exchange.empty? ? primary_exchange : nil), + @description.present? ? " (#{@description}) " : nil, + ].compact + "" + end + + end +end diff --git a/lib/models/ib/underlying.rb b/models/ib/underlying.rb similarity index 80% rename from lib/models/ib/underlying.rb rename to models/ib/underlying.rb index ec26885..11e242d 100644 --- a/lib/models/ib/underlying.rb +++ b/models/ib/underlying.rb @@ -1,10 +1,6 @@ module IB - if defined?(Underlying) - puts "Underlying already a #{defined?(Underlying)}" - else - # Calculated characteristics of underlying Contract (volatile) - class Underlying < IB::Model + class Underlying < IB::Base include BaseProperties has_one :contract @@ -21,7 +17,7 @@ def default_attributes # Serialize under_comp parameters def serialize - [true, con_id, delta, price] + [con_id, delta, price] end # Comparison @@ -33,5 +29,4 @@ def == other end # class Underlying UnderComp = Underlying - end -end # module IB +end diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb new file mode 100644 index 0000000..ec2abc7 --- /dev/null +++ b/plugins/ib/advanced-account.rb @@ -0,0 +1,442 @@ +module IB +=begin + +Plugin that provides helper methods for orders + + +Public API +========== + +Extends IB::Account + +=end + + module Advanced + + + def account_data_scan search_key, search_currency=nil + if search_currency.present? + account_values.find_all{|x| x.key.match( search_key ) && x.currency == search_currency.upcase } + else + account_values.find_all{|x| x.key.match( search_key ) } + end + end + + + +=begin rdoc +given any key of local_id, perm_id or order_ref +and an optional status, which can be a string or a +regexp ( status: /mitted/ matches Submitted and Presubmitted) +the last associated Order-record is returned. + +Thus if several Orders are placed with the same order_ref, the active one is returned + +(If multible keys are specified, local_id preceeds perm_id) + +=end + def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/, contract: nil, con_id: nil + search_option = [ local_id.present? ? [:local_id , local_id] : nil , + perm_id.present? ? [:perm_id, perm_id] : nil, + order_ref.present? ? [:order_ref , order_ref ] : nil ].compact.first + matched_items = if search_option.nil? + orders # select all orders of the current account + else + key,value = search_option + orders.find_all{|x| x[key].to_i == value.to_i } + end + + if contract.present? + if contract.con_id.zero? && !contract.is_a?( IB::Bag ) + contract = contract.verify.first + end + matched_items = matched_items.find_all{|o| o.contract.essential == contract.essential } + elsif con_id.present? + matched_items = matched_items.find_all{|o| o.contract.con_id == con_id } + end + + if status.present? + status = Regexp.new(status) unless status.is_a? Regexp + matched_items.detect{|x| x.order_state.status =~ status } + else + matched_items.last # return the last item + end + end + + +=begin rdoc +requires an IB::Order as parameter. + +If attached, the associated IB::Contract is used to specify the tws-command + +The associated Contract overtakes the specified (as parameter) + +auto_adjust: Limit- and Aux-Prices are adjusted to Min-Tick + +convert_size: The action-attribute (:buy :sell) is associated according the content of :total_quantity. + + +The parameter «order» is modified! + +It can further used to modify and eventually cancel + + +Example + + j36 = IB::Stock.new symbol: 'J36', exchange: 'SGX' + order = IB::Limit.order size: 100, price: 65.5 + g = IB::Connection.current.clients.last + + g.preview contract: j36, order: order + => {:init_margin=>0.10864874e6, + :maint_margin=>0.9704137e5, + :equity_with_loan=>0.97877973e6, + :commission=>0.524e1, + :commission_currency=>"USD", + :warning=>"" + + g.place order: order + => 67 # returns local_id + order.contract # updated (and verifired) contract-record + => #9534669, + :exchange=>"SGX", + :right=>"", + :include_expired=>false}> + + order.limit_price = 65 # set new price + g.modify order: order # and transmit + => 67 # returns local_id + + g.locate_order( local_id: {a number} ) + => returns the assigned order-record for inspection + + g.cancel order: order + # logger output: 05:17:11 Cancelling 65 New #250/ from 3000/DU167349> +=end + + def place_order order:, contract: nil, auto_adjust: true, convert_size: true + result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } + qualified_contract = ->(c) do + c.is_a?(IB::Contract) && + #·IB::Symbols are always qualified. They carry a description-field + ( c.description.present? || !c.con_id.to_i.zero? || + (c.con_id.to_i <0 && c.sec_type == :bag ) ) # bags that carry a negative con_id are qualified + end + + # assign qualificated contract to the order object if not present + order.contract ||= if qualified_contract[ contract ] + contract + else + contract.verify.first + end + # disable auto-adjust if min_tick is not available + auto_adjust = false if order.contract.contract_detail.nil? + + error "No valid contract given" unless order.contract.is_a?(IB::Contract) + + ## sending of plain vanilla IB::Bags will fail using account.place, unless a (negative) con-id is provided! + error "place order: ContractVerification failed. No con_id assigned" unless qualified_contract[order.contract] or contract.nil? + + # declare some variables + ib = IB::Connection.current + wrong_order = nil + the_local_id = nil + q = Queue.new + + ### Handle Error messages + ### Default action: log message and raise IB::TransmissionError + sa = ib.subscribe( :Alert ) do | msg | + if msg.error_id == the_local_id + wrong_order = msg.message + if msg.code == 110 # The price does not confirm to the minimum price variation for this contract + if auto_adjust + wrong_order = nil + the_local_id = -1 + ib.logger.info "adjusting order-price" + else + ib.logger.error "The price #{order.limit_price}/ #{order.aux_price} not confirm to the minimum price variation for #{order.contract.to_human}" + end + elsif [ 201, # Order rejected, No Trading permissions + 203, # Security is not allowed for trading + 325, # Discretionary Orders are not supported for this combination of order-type and exchange + 355, # Order size does not conform to market rule + 361, 362, 363, 364, # invalid trigger or stop-price + 388, # Order size x is smaller than the minimum required size of yy. + ].include? msg.code + ib.logger.error msg.message + end + q.close # closing the queue indicates that no order was transmitted + end + end + # transfer the received openOrder to the queue + sb = ib.subscribe( :OpenOrder ){|m| q << m.order if m.order.local_id.to_i == the_local_id.to_i } + # modify order (parameter) + order.account = account # assign the account_id to the account-field of IB::Order + self.orders.save_insert order, :order_ref + order.auto_adjust if ib.plugins.include?( "auto-adjust" ) && auto_adjust + if convert_size + order.action = order.total_quantity.to_d < 0 ? :sell : :buy unless order.action == :sell + logger.info{ "Converted ordersize to #{order.total_quantity} and triggered a #{order.action} order"} if order.total_quantity.to_d < 0 + order.total_quantity = order.total_quantity.to_d.abs + end + # con_id and exchange fully qualify a contract, no need to transmit other data + # if no contract is passed to order.place, order.contract is used for placement + # ... delegated to order#modify... +# the_contract = order.contract.con_id.to_i > 0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil + contract = order.contract + order = order.then{|x| x.contract = nil; x } + loop do + the_local_id = ib.place_order order, contract # return the local_id + # if transmit is false, just include the local_id in the order-record + Thread.new{ if order.transmit || order.what_if then sleep 1 else sleep 0.001 end ; q.close } + tws_answer = q.pop + + adjust_price = ->(p) do + if order.action == :sell + p + contract.contract_detail.min_tick + else + p - contract.contract_detail.min_tick + end + end + + if q.closed? + if wrong_order.present? + raise IB::SymbolError, wrong_order + elsif the_local_id.present? + if the_local_id < 0 # auto-adjust condition + order.local_id = nil # reset order record + order.aux_price = adjust_price.call( order.aux_price ) unless order.aux_price.to_i.zero? + order.limit_price = adjust_price.call( order.limit_price ) unless order.limit_price.to_i.zero? + else + order.local_id = the_local_id + end + else + error " #{order.to_human} is not transmitted properly", :symbol + end + else + order=tws_answer # return order-record received from tws + end + break unless order.local_id.nil? + q = Queue.new # reset queue + end + order.contract = contract + ib.unsubscribe sa + ib.unsubscribe sb + order # return the order-record + end # place + + + # shortcut to enable + # account.place order: {} , contract: {} + # account.preview order: {} , contract: {} + # account.modify order: {} + alias place place_order + +=begin #rdoc +Account#ModifyOrder operates in two modi: + +First: The order is specified via local_id, perm_id or order_ref. + It is checked, whether the order is still modifyable. + Then the Order ist provided through the block. Any modification is done there. + Important: The Block has to return the modified IB::Order + +Second: The order can be provided as parameter as well. This will be used +without further checking. The block is now optional. + Important: The OrderRecord must provide a valid Contract. + +The simple version does not adjust the given prices to tick-limits. +This has to be done manually in the provided block +=end + + + def modify_order local_id: nil, order_ref: nil, order:nil, contract: nil + + result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } + order ||= locate_order( local_id: local_id, + status: /ubmitted/ , + order_ref: order_ref ) + if order.is_a? IB::Order + order.modify + else + error "No suitable IB::Order provided/detected. Instead: #{order.inspect}" + end + end + + alias modify modify_order + +# Preview + # + # Submits a "WhatIf" Order + # + # Returns the presubmitted order record, where the local_id is erased and the what_if-flag is false + # + # + # output of the results: + # u = Connection.current.clients.last + # o = Limit.order ... + # c = Contract.new ... + # preview = u.preview contract: c, order: o + # puts preview.order_state.forcast + # + # The returned order can be used as argument for a subsequent order-placement + # i.e + # fits_margin_minium = ->(x) do + # buffer = x[equity_with_loan] - x[:init_margin] + # net_liquidation = account_data_scan( /NetLiq/ ).first.value.to_i + # buffer > net_liquidation * 0.1 # 90 Percent margin usage is aceptable + # end + # u.preview( contract: c, order: o ) + # .check_margin(u){|y| fits_margin_minumum[ y.order_state.forcast ] } &.place + # + # + # The order received from the TWS is also kept in account.orders + # + # Raises IB::SymbolError if the Order could not be placed properly + # + def preview order:, contract: nil, **args_which_are_ignored + # to_do: use a copy of order instead of temporary setting order.what_if + q = Queue.new + ib = IB::Connection.current + contract = order.contract if contract.nil? + order = order.then{|x| x.contract = nil; x } + the_local_id = nil + # put the order into the queue (and exit) if the event is fired + req = ib.subscribe( :OpenOrder ) do |m| + q << m.order if m.order.local_id.to_i == the_local_id.to_i && !m.order.order_state.init_margin_after.nil? + end + + order.what_if = true + order.account = account + the_local_id = ib.place_order order, contract + Thread.new{ sleep 2 ; q.close } # wait max 2 sec. + returned_order = q.pop + ib.unsubscribe req +# order.what_if = false # reset what_if flag +# order.local_id = nil # reset local_id to enable re-using the order-object for placing + raise IB::SymbolError,"(Preview-) #{order.to_human} is not transmitted properly" if q.closed? + #order.order_state.forcast # return_value + returned_order.local_id = nil + returned_order.what_if = false + returned_order.contract = contract + returned_order + end + + +# closes the contract by submitting an appropriate order + # the action- and total_amount attributes of the assigned order are overwritten. + # + # if a ratio-value (0 ..1) is specified in _order.total_quantity_ only a fraction of the position is closed. + # Other values are silently ignored + # + # if _reverse_ is specified, the opposite position is established. + # Any value in total_quantity is overwritten + # + # returns the order transmitted + #v # raises an IB::Error if no PortfolioValues have been loaded to the IB::Account + def close order:, contract: nil, reverse: false, **args_which_are_ignored + error "must only be called after initializing portfolio_values " if portfolio_values.blank? + contract_size = ->(c) do # note: portfolio_value.position is either positiv or negativ + if c.con_id <0 # Spread + p = portfolio_values.detect{|p| p.contract.con_id ==c.legs.first.con_id} &.position.to_i + p/ c.combo_legs.first.weight unless p.to_i.zero? + else + portfolio_values.detect{|x| x.contract.con_id == c.con_id} &.position.to_i # nil.to_i -->0 + end + end + + order.contract = contract.verify.first unless contract.nil? || contract.con_id.to_i <=0 + error "Cannot transmit the order – No Contract given " unless order.contract.is_a?( IB::Contract ) + + the_quantity = if reverse + -contract_size[order.contract] * 2 + elsif order.total_quantity.abs < 1 && !order.total_quantity.zero? + -contract_size[order.contract] * order.total_quantity.abs + else + -contract_size[order.contract] + end + if the_quantity.zero? + logger.info{ "Cannot close #{order.contract.to_human} - no position detected"} + else + order.total_quantity = the_quantity + order.action = nil + order.local_id = nil # in any case, close is a new order + logger.info { "Order modified to close, reduce or revese position: #{order.to_human}" } + place order: order, convert_size: true + end + end + +# just a wrapper to the Gateway-cancel-order method + def cancel order: + Connection.current.cancel_order order.local_id + end + + ## ToDo ... needs adaption ! + #returns an hash where portfolio_positions are grouped into Watchlists. + # + # Watchlist => [ contract => [ portfoliopositon] , ... ] ] + # + def organize_portfolio_positions the_watchlistsi #= IB::Gateway.current.active_watchlists + the_watchlists = [ the_watchlists ] unless the_watchlists.is_a?(Array) + self.focuses = portfolio_values.map do | pw | # iterate over pw + ref_con_id = pw.contract.con_id + z = the_watchlists.map do | w | # iterate over w and assign to z + watchlist_contract = w.find do |c| # iterate over c + if c.is_a? IB::Bag + c.combo_legs.map( &:con_id ).include?( ref_con_id ) + else + c.con_id == ref_con_id + end + end rescue nil + watchlist_contract.present? ? [w,watchlist_contract] : nil + end.compact + + z.empty? ? [ IB::Symbols::Unspecified, pw.contract, pw ] : z.first + pw + end.group_by{|a,_,_| a }.map{|x,y|[x, y.map{|_,d,e|[d,e]}.group_by{|e,_| e}.map{|f,z| [f, z.map(&:last)]} ] }.to_h + # group:by --> [a,b,c] .group_by {|_g,_| g} --->{ a => [a,b,c] } + # group_by+map --> removes "a" from the resulting array + end + + + def locate_contract con_id + contracts.detect{|x| x.con_id.to_i == con_id.to_i } + end + + ## returns the contract definition of an complex portfolio-position detected in the account + def complex_position con_id + con_id = con_id.con_id if con_id.is_a?(IB::Contract) + focuses.map{|x,y| y.detect{|x,y| x.con_id.to_i== con_id.to_i} }.compact.flatten.first + end + end # module Advanced + ## + # in the console (call gateway with watchlist: [:Spreads, :BuyAndHold]) +#head :001 > .clients.first.focuses.to_a.to_human +#Unspecified +# +# +# +# +# +# +# +# +# +# +#BuyAndHold +# +# +# +# +#Spreads +# +# +# +# => nil +# + # + # load managed-accounts first and switch to gateway-mode +Connection.current.activate_plugin 'managed-accounts' +Connection.current.activate_plugin 'order-flow' +class Account + include Advanced +end +end ## module IB diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb new file mode 100644 index 0000000..e69de29 diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb new file mode 100644 index 0000000..8176ff9 --- /dev/null +++ b/plugins/ib/connection-tools.rb @@ -0,0 +1,122 @@ +module IB + +=begin +Plugin for advanced Connections + +Public API +========== + +Extends IB::Connection + +Provides + * IB::Connection.current.check_connection + * IB::Connection.current.safe_connect + * IB::Connection.reconect + + +=end + + module ConnectionTools + # Handy method to ensure that a connection is established and active. + # + # The connection is reset on the IB-side at least once a day. Then the + # IB-Ruby-Connection has to be reestablished, too. + # + # check_connection reconnects if necessary and returns false if the connection is lost. + # + # Individial subscriptions have to be placed **after** checking the connection! + # + # It delays the process by 6 ms (500 MBit Cable connection, loc. Europe) + # + # a = Time.now; IB::Connection.current.check_connection; b= Time.now ;b-a + # => 0.00066005 + # + def check_connection + q = Queue.new + count = 0 + result = nil + z= subscribe( :CurrentTime ) { q.push true } + loop do + begin + send_message(:RequestCurrentTime) # 10 ms ## + th = Thread.new{ sleep 0.1 ; q.push nil } + result = q.pop + count+=1 + break if result || count > 10 + rescue IOError, Errno::ECONNREFUSED # connection lost + count +=1 + retry + rescue IB::Error # not connected + logger.info{"not connected ... trying to reconnect "} + reconnect + z= subscribe( :CurrentTime ) { q.push true } + count = 0 + retry + rescue Workflow::NoTransitionAllowed + logger.warn{ "Reconnect is not possible, actual state: #{workflow_state} cannot be reached after disconnection"} + raise + end + end + unsubscribe z + result # return value + end + + # + # Tries to connect to the api. If the connection could not be established, waits + # 10 sec. or one minute and reconnects. + # + # Unsuccessful connecting attemps are logged. + # + # + protected + def try_connection maximal_count_of_retry=100 + + i= -1 + begin + _try_connection + rescue Errno::ECONNREFUSED => e + i+=1 + if i < maximal_count_of_retry + if i.zero? + logger.info 'No TWS!' + else + logger.info {"No TWS Retry #{i}/ #{maximal_count_of_retry} " } + end + sleep i<50 ? 10 : 60 # Die ersten 50 Versuche im 10 Sekunden Abstand, danach 1 Min. + retry + else + logger.info { "Giving up!!" } + return false + end + rescue Errno::EHOSTUNREACH => e + error "Cannot connect to specified host #{e}", :reader, true + return false + rescue SocketError => e + error 'Wrong Adress, connection not possible', :reader, true + return false + rescue IB::Error => e + logger.info e + end + self # return connection + end # def + + def submit_to_alert_1102 + current.subscribe( :Alert ) do + if [2102, 1101].include? msg.id.to_i # Connectivity between IB and Trader Workstation + #has been restored - data maintained. + current.disconnect! + sleep 0.1 + current.check_connection + end + end + + end + end + + class Connection + alias _try_connection try_connection + include ConnectionTools + end + + +end diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb new file mode 100644 index 0000000..d1fd36d --- /dev/null +++ b/plugins/ib/eod.rb @@ -0,0 +1,326 @@ +module IB +require 'active_support/core_ext/date/calculations' +require 'csv' + +=begin + +Plugin to support EndOfDay OHLC-Data for a contract + +Public API +========== + +Extends IB::Contract + +* eod + + * request EndOfDay historical data + + * returns an Array of OHLC-EOD-Records or a Polars-Dataframe populated with OHLC-Records for the contract + and populates IB::Contract#bars + + +* get_bars + + * request historical data for custom ohlc-timeframes, + +* from_csv and to_csv + + * store and retrieve ohlc-data + + +=end + + module Eod + module BuisinesDays + # https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days + + # Calculates the number of business days in range (start_date, end_date] + # + # @param start_date [Date] + # @param end_date [Date] + # + # @return [Fixnum] + def self.business_days_between(start_date, end_date) + days_between = (end_date - start_date).to_i + return 0 unless days_between > 0 + + # Assuming we need to calculate days from 9th to 25th, 10-23 are covered + # by whole weeks, and 24-25 are extra days. + # + # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 # 1 2 3 4 5 + # 6 7 8 9 10 11 12 # 6 7 8 9 ww ww ww + # 13 14 15 16 17 18 19 # ww ww ww ww ww ww ww + # 20 21 22 23 24 25 26 # ww ww ww ww ed ed 26 + # 27 28 29 30 31 # 27 28 29 30 31 + whole_weeks, extra_days = days_between.divmod(7) + + unless extra_days.zero? + # Extra days start from the week day next to start_day, + # and end on end_date's week date. The position of the + # start date in a week can be either before (the left calendar) + # or after (the right one) the end date. + # + # Su Mo Tu We Th Fr Sa # Su Mo Tu We Th Fr Sa + # 1 2 3 4 5 # 1 2 3 4 5 + # 6 7 8 9 10 11 12 # 6 7 8 9 10 11 12 + # ## ## ## ## 17 18 19 # 13 14 15 16 ## ## ## + # 20 21 22 23 24 25 26 # ## 21 22 23 24 25 26 + # 27 28 29 30 31 # 27 28 29 30 31 + # + # If some of the extra_days fall on a weekend, they need to be subtracted. + # In the first case only corner days can be days off, + # and in the second case there are indeed two such days. + extra_days -= if start_date.tomorrow.wday <= end_date.wday + [start_date.tomorrow.sunday?, end_date.saturday?].count(true) + else + 2 + end + end + + (whole_weeks * 5) + extra_days + end + end + + # eod + # + # Receive EOD-Data and store the data in the `:bars`-property of IB::Contract + # + # contract.eod duration: {String or Integer}, start: {Date}, to: {Date}, what: {see below}, polars: {true|false} + # + # + # + # The Enddate has to be specified (as Date Object), `:to`, default: Date.today + # + # The Duration can either be a String "yx D", "yd W", "yx M" or an Integer ( implies "D"). + # *notice* "W" fetchtes weekly and "M" monthly bars + # + # A start date can be given with the `:start` parameter. + # + # The parameter `:what` specifies the kind of received data. + # + # Valid values: ( /lib/ib/constants.rb --> DATA_TYPES ) + # :trades, :midpoint, :bid, :ask, :bid_ask, + # :historical_volatility, :option_implied_volatility, + # :option_volume, :option_open_interest + # + # Polars DataFrames + # ----------------- + # If »polars: true« is specified the response is stored as PolarsDataframe. + # For further processing: https://github.com/ankane/polars-ruby + # https://pola-rs.github.io/polars/py-polars/html/index.html + # + # Error-handling + # -------------- + # * Basically all Errors simply lead to log-entries: + # * the contract is not valid, + # * no market data subscriptions + # * other servers-side errors + # + # If the duration is longer then the maximum range, the response is + # cut to the maximum allowed range + # + # Customize the result + # -------------------- + # The results are stored in the `:bars` property of the contract + # + # + # Limitations + # ----------- + # To identify a request, the con_id of the asset is used + # Thus, parallel requests of a single asset with different time-frames will fail + # + # Examples + # -------- + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2019,10,9), duration: 3, polars: true) + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪════════╪═════════╪════════╡ + # │ 2019-10-08 ┆ 148.62 ┆ 149.37 ┆ 146.11 ┆ 146.45 ┆ 156625 ┆ 146.831 ┆ 88252 │ + # │ 2019-10-09 ┆ 147.18 ┆ 148.0 ┆ 145.38 ┆ 145.85 ┆ 94337 ┆ 147.201 ┆ 51294 │ + # │ 2019-10-10 ┆ 146.9 ┆ 148.74 ┆ 146.87 ┆ 148.24 ┆ 134549 ┆ 147.792 ┆ 71084 │ + # └────────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2021,10,9), duration: '3W', polars: true) + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪════════╡ + # │ 2021-10-01 ┆ 223.99 ┆ 227.68 ┆ 216.12 ┆ 222.8 ┆ 1295495 ┆ 222.226 ┆ 792711 │ + # │ 2021-10-08 ┆ 221.4 ┆ 224.95 ┆ 216.76 ┆ 221.65 ┆ 1044233 ┆ 220.855 ┆ 621984 │ + # │ 2021-10-15 ┆ 220.69 ┆ 228.41 ┆ 218.94 ┆ 225.05 ┆ 768065 ┆ 223.626 ┆ 437817 │ + # └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2022,10,1), duration: '3M', polars: true) + # shape: (3, 8) + # ┌────────────┬────────┬────────┬────────┬────────┬─────────┬─────────┬─────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪════════╪════════╪════════╪════════╪═════════╪═════════╪═════════╡ + # │ 2022-09-30 ┆ 181.17 ┆ 191.37 ┆ 162.77 ┆ 165.16 ┆ 4298969 ┆ 175.37 ┆ 2202407 │ + # │ 2022-10-31 ┆ 165.5 ┆ 184.24 ┆ 162.5 ┆ 183.5 ┆ 4740014 ┆ 173.369 ┆ 2474286 │ + # │ 2022-11-30 ┆ 184.51 ┆ 189.56 ┆ 174.11 ┆ 188.19 ┆ 3793861 ┆ 182.594 ┆ 1945674 │ + # └────────────┴────────┴────────┴────────┴────────┴─────────┴─────────┴─────────┘ + # + # puts Stock.new( symbol: :iwm).eod( start: Date.new(2020,1,1), duration: '3M', what: :option_implied_vol, polars: true + # atility ) + # shape: (3, 8) + # ┌────────────┬──────────┬──────────┬──────────┬──────────┬────────┬──────────┬────────┐ + # │ time ┆ open ┆ high ┆ low ┆ close ┆ volume ┆ wap ┆ trades │ + # │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ + # │ date ┆ f64 ┆ f64 ┆ f64 ┆ f64 ┆ i64 ┆ f64 ┆ i64 │ + # ╞════════════╪══════════╪══════════╪══════════╪══════════╪════════╪══════════╪════════╡ + # │ 2019-12-31 ┆ 0.134933 ┆ 0.177794 ┆ 0.115884 ┆ 0.138108 ┆ 0 ┆ 0.178318 ┆ 0 │ + # │ 2020-01-31 ┆ 0.139696 ┆ 0.190494 ┆ 0.120646 ┆ 0.185732 ┆ 0 ┆ 0.19097 ┆ 0 │ + # │ 2020-02-28 ┆ 0.185732 ┆ 0.436549 ┆ 0.134933 ┆ 0.39845 ┆ 0 ┆ 0.435866 ┆ 0 │ + # └────────────┴──────────┴──────────┴──────────┴──────────┴────────┴──────────┴────────┘ + # + def eod start: nil, to: nil, duration: nil , what: :trades, polars: false + + # error "EOD:: Start-Date (parameter: to) must be a Date-Object" unless to.is_a? Date + normalize_duration = ->(d) do + error "incompatible duration: #{d.class}" unless d.is_a?(Integer) || d.is_a?(String) + if d.is_a?(Integer) || !["D","M","W","Y"].include?( d[-1].upcase ) + d.to_i.to_s + "D" + else + d.gsub(" ","") + end.insert(-2, " ") + end + + get_end_date = -> do + d = normalize_duration.call(duration) + case d[-1] + when "D" + start + d.to_i - 1 + when 'W' + Date.commercial( start.year, start.cweek + d.to_i - 1, 1) + when 'M' + Date.new( start.year, start.month + d.to_i - 1 , start.day ) + end + end + + if to.nil? + # case eod start= Date.new ... + to = if start.present? && duration.nil? + # case eod start= Date.new + duration = BuisinesDays.business_days_between(start, Date.today).to_s + "D" + Date.today # assign to var: to + elsif start.present? && duration.present? + # case eod start= Date.new , duration: 'nN' + get_end_date.call # assign to var: to + elsif duration.present? + # case start is not present, we are collecting until the present day + Date.today # assign to var: to + else + duration = "1D" + Date.today + end + end + if duration.nil? + duration = BuisinesDays.business_days_between(start, to) + end + + barsize = case normalize_duration.call(duration)[-1].upcase + when "W" + :week1 + when "M" + :month1 + else + :day1 + end + + + get_bars( to.to_ib(time_zone) , normalize_duration[duration], barsize, what, polars ) + + end # def + + # creates (or overwrites) the specified file (or symbol.csv) and saves bar-data + def to_csv file: "#{symbol}.csv" + if bars.present? + headers = bars.first.invariant_attributes.keys + CSV.open( file, 'w' ) {|f| f << headers ; bars.each {|y| f << y.invariant_attributes.values } } + end + end + + # read csv-data into bars + def from_csv file: nil + file ||= "#{symbol}.csv" + self.bars = [] + CSV.foreach( file, headers: true, header_converters: :symbol) do |row| + self.bars << IB::Bar.new( **row.to_h ) + end + end + +# get_bars:: Helper method to fetch historical data +# +# parameter: end_date_time:: A string representing the last datum to fetch. +# Date.to_ib and Time.to_ib return the correct format +# duration:: A String "yx D", "yd W", "yx M" +# barsize:: A valid BAR_SIZES-entry (/lib/ib/constants.rb) +# what_to_show:: A valid DATA_TYPES-entry (/lib/ib/constants.rb) +# polars:: Flag to indicate if a polars-dataframe should be returned + + def get_bars(end_date_time, duration, bar_size, what_to_show, polars) + + tws = IB::Connection.current + received = Queue.new + r = nil + # the hole response is transmitted at once! + a = tws.subscribe(IB::Messages::Incoming::HistoricalData) do |msg| + if msg.request_id == con_id + self.bars = if polars + # msg.results.each { |entry| puts " #{entry}" } + Polars::DataFrame.new msg.results.map( &:invariant_attributes ) + else + msg.results + end + end + received.push Time.now + end + b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg| + if [321,162,200,354].include? msg.code + tws.logger.warn msg.message + # TWS Error 200: No security definition has been found for the request + # TWS Error 354: Requested market data is not subscribed. + # TWS Error 162: Historical Market Data Service error + # TWS Error 321: Error validating request.-'bK' : cause - + # Historical data requests for durations longer than 365 days must be made in years. + + received.close + end + end + + + tws.send_message IB::Messages::Outgoing::RequestHistoricalData.new( + :request_id => con_id, + :contract => self, + :end_date_time => end_date_time, + :duration => duration, # see ib/messages/outgoing/bar_request.rb => max duration for 5sec bar lookback is 10 000 - i.e. will yield 2000 bars + :bar_size => bar_size, # IB::BAR_SIZES.key(:hour) + :what_to_show => what_to_show, + :use_rth => 0, + :format_date => 2, + :keep_up_todate => 0) + + received.pop # blocks until a message is ready on the queue or the queue is closed + + tws.unsubscribe a + tws.unsubscribe b + + block_given? ? bars.map{|y| yield y} : bars # return bars or result of block + + end # def + end # module eod + + class Contract + include Eod + end # class +end # module IB + diff --git a/plugins/ib/greeks.rb b/plugins/ib/greeks.rb new file mode 100644 index 0000000..c81f7d5 --- /dev/null +++ b/plugins/ib/greeks.rb @@ -0,0 +1,102 @@ +module IB +=begin + +Plugin to fetch Option Greeks + +Provides Option.request_greeks and + Option.greeks to display the fetched data + +=end + + module Greeks + +# Ask for the Greeks and implied Vola +# +# The result can be customized by a provided block. +# +# IB::Symbols::Options.aapl.greeks{ |x| x } +# -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3} +# +# Possible values for Parameter :what --> :all :model, :bid, :ask, :bidask, :last +# + def request_greeks delayed: true, what: :model, thread: false + + tws = Connection.current # get the initialized ib-ruby instance + # define requested tick-attributes + request_data_type = IB::MARKET_DATA_TYPES.rassoc( delayed ? :frozen_delayed : :frozen ).first + # possible types = [ [ :delayed_model_option , :model_option ] , [:delayed_last_option , :last_option ], + # [ :delayed_bid_option , :bid_option ], [ :delayed_ask_option , :ask_option ]] + tws.send_message :RequestMarketDataType, :market_data_type => request_data_type + tickdata = [] + + self.greek = OptionDetail.new if greek.nil? + greek.updated_at = Time.now + greek.option = self + queue = Queue.new + + #keep the method-call running until the request finished + #and cancel subscriptions to the message handler + # method returns the (running) thread + th = Thread.new do + the_id = nil + # subscribe to TickPrices + s_id = tws.subscribe(:TickSnapshotEnd) { |msg| queue.push(true) if msg.ticker_id == the_id } + e_id = tws.subscribe(:Alert){|x| queue.push(false) if [200,353].include?( x.code) && x.error_id == the_id } + t_id = tws.subscribe( :TickSnapshotEnd, :TickPrice, :TickString, :TickSize, :TickGeneric, :MarketDataType, :TickRequestParameters ) {|msg| msg } + # TWS Error 200: No security definition has been found for the request + # TWS Error 354: Requested market data is not subscribed. + + sub_id = tws.subscribe(:TickOption ) do |msg| #, :TickSize, :TickGeneric do |msg| + if msg.ticker_id == the_id # && tickdata.is_a?(Array) # do nothing if tickdata have already gathered + case msg.type + when /ask/ + greek.ask_price = msg.option_price unless msg.option_price.nil? + tickdata << msg if [ :all, :ask, :bidask ].include?( what ) + + when /bid/ + greek.bid_price = msg.option_price unless msg.option_price.nil? + tickdata << msg if [ :all, :bid, :bidask ].include?( what ) + when /last/ + tickdata << msg if msg.type =~ /last/ + when /model/ + # transfer attributs from TickOption to OptionDetail + bf =[ :option_price, :implied_volatility, :under_price, :pv_dividend ] + (bf + msg.greeks.keys).each{ |a| greek.send( a.to_s+"=", msg.send( a)) } + tickdata << msg if [ :all, :model ].include?( what ) + end + # fast entry abortion ---> daiabled for now + # queue.push(true) if tickdata.is_a?(IB::Messages::Incoming::TickOption) || (tickdata.size == 2 && what== :bidask) || (tickdata.size == 4 && what == :all) + end + end # if sub_id + + # initialize »the_id« that is used to identify the received tick messages + # by firing the market data request + iji = 0 + loop do + the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true + + result = queue.pop + # reduce :close_price delayed_close to close a.s.o + if result == false + Connection.logger.info{ "#{to_human} --> No Marketdata received " } + else + self.misc = tickdata if thread # store internally if in thread modus + end + break if !tickdata.empty? || iji > 10 + iji = iji + 1 + Connection.logger.info{ "OptionGreeks::#{to_human} --> delayed processing. Trying again (#{iji}) " } + end + tws.unsubscribe sub_id, s_id, e_id, t_id + end # thread + if thread + th # return thread + else + th.join + greek + end + end + end + class Option + include Greeks + end +end diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb new file mode 100644 index 0000000..1614585 --- /dev/null +++ b/plugins/ib/managed-accounts.rb @@ -0,0 +1,257 @@ +module IB + +=begin + +Plugin for Managed Accounts + +Provides `clients` and `advisor` objects (Type: IB::Account) that contain account-specific data. + +Public Api +========== + +* InitializeManagedAccounts + * populates @accounts through RequestFA + * should be called instead of `connect` + + +* GetAccountData + * requests account- and portfolio-data and associates them to the clients + +* provides + * client.account_values + * client.portfolio_values + * client.contracts + + +The plugin should be activated **before** the connection attempt. + +**IB::Connection.current.initialize_manage_acounts performs a `connect` to the tws-server**` + + +Standard usage + + ib = IB::Connection.new + ib.activate_plugin 'managed-accounts' + ib.initialize_managed_accounts! # connects to the tws + ib.get_account_data # populates c.clients + account = ib.clients.first + puts account.portfolio_values.as_table + +=end + +module ManagedAccounts + + +=begin +clients returns a list of Account-Objects + +If only one Account is present, Client and Advisor are identical. +=end + def clients + @accounts.find_all &:user? + end + +# is the account a financial advisor + def fa? + !(advisor == clients.first) + end + + +=begin + The Advisor is always the first account +=end + def advisor + @accounts.first + end + + +=begin +--------------------------- GetAccountData -------------------------------------------- +Queries for Account- and PortfolioValues +The parameter can either be the account_id, the IB::Account-Object or +an Array of account_id and IB::Account-Objects. + +Resets Account#portfolio_values and -account_values + +Raises an IB::TransmissionError if the account-data are not transmitted in time (1 sec) + +Raises an IB::Error if less then 100 items are received. +=end + def get_account_data *accounts, **compatibily_argument + + subscription = subscribe_account_updates( continuously: false ) + download_end = nil # declare variable + received_array_status = received + self.received = false + + accounts = clients if accounts.empty? + logger.warn{ "No active account present. AccountData are NOT requested" } if accounts.empty? + # Account-infos have to be requested sequentially. + # subsequent (parallel) calls kill the former on the tws-server-side + # In addition, there is no need to cancel the subscription of an request, as a new + # one overwrites the active one. + accounts.each do | ac | + account = ac.is_a?( IB::Account ) ? ac : clients.find{|x| x.account == ac } + error( "No Account detected " ) unless account.is_a? IB::Account + # don't repeat the query until 170 sec. have passed since the previous update + if account.last_updated.nil? || ( Time.now - account.last_updated ) > 170 # sec + logger.debug{ "#{account.account} :: Erasing Account- and Portfolio Data " } + logger.debug{ "#{account.account} :: Requesting AccountData " } + + q = Queue.new + download_end = subscribe( :AccountDownloadEnd ) do | msg | + q.push true if msg.account_name == account.account + end + # reset account and portfolio-values + account.portfolio_values = [] + account.account_values = [] + # Data are gathered asynchron through the active subscription defined in `subscribe_account_updates` + send_message :RequestAccountData, subscribe: true, account_code: account.account + + th = Thread.new{ sleep 10 ; q.close } # close the queue after 10 seconds + q.pop # wait for the data (or the closing event) + + if q.closed? + error "No AccountData received", :reader + else + q.close + unsubscribe download_end + end + +# account.organize_portfolio_positions unless IB::Gateway.current.active_watchlists.empty? + else + logger.info{ "#{account.account} :: Using stored AccountData " } + end + end + send_message :RequestAccountData, subscribe: false ## do this only once + unsubscribe subscription + + self.received = received_array_status + rescue IB::TransmissionError => e + unsubscribe download_end unless download_end.nil? + unsubscribe subscription + raise + end + + + def all_contracts + clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id) + end + + +=begin +--------------------------- InitializeManageAccounts ---------------------------------- + +If initiated with the parameter `force: true`, any active connection is terminated. +All subscriptiona are lost. The connection ist then re-established to initiate the +transmission of available managed-accounts by the tws. + +=end + protected + def initialize_managed_accounts( force: false ) + queue = Queue.new + if connected? + disconnect! + sleep(0.1) + end + try_connection! + @accounts = [] + # in case of advisor-accounts: proper initialiastion of account records + rec_id = subscribe( :ReceiveFA ) do |msg| + msg.accounts.each do |a| + account_data( a.account ){| the_account | the_account.update_attribute :alias, a.alias } unless a.alias.blank? + end + logger.info { "Accounts initialized \n #{@accounts.map( &:to_human ).join " \n " }" } + queue.push(true) + end + + # initialisation of Account after a successful connection + man_id = subscribe( :ManagedAccounts ) do |msg| + @accounts = msg.accounts + send_message( :RequestFA, fa_data_type: 3) + end + + # single accounts return an alert message + error_id = subscribe( :Alert ){|x| queue.push(false) if x.code == 321 } + + result = queue.pop + unsubscribe man_id, rec_id, error_id + + @accounts + + end # def + + # The subscription method should called only once per session. + # It places subscribers to AccountValue and PortfolioValue Messages, which should remain + # active through the session. + # + # The method returns the subscription-number. + # + # thus + # subscription = subscribe_account_updates + # # some code + # IB::Connection.current.unsubscribe subscription + # + # clears the subscription + # + + private + def subscribe_account_updates continuously: true + + add_or_update = ->(apv, new_hash) do + existing_index = apv.index { |h| h.contract.con_id == new_hash.contract.con_id } + if existing_index + apv[existing_index] = new_hash + else + apv << new_hash + end + end + subscribe( :AccountValue, :PortfolioValue,:AccountDownloadEnd ) do | msg | + account_data( msg.account_name ) do | account | # enter mutex controlled zone + case msg + when IB::Messages::Incoming::AccountValue + account.account_values << msg.account_value + account.update_attribute :last_updated, Time.now + IB::Connection.logger.debug { "#{account.account} :: #{msg.account_value.to_human }"} + when IB::Messages::Incoming::AccountDownloadEnd + if account.account_values.size > 10 + account.update_attribute :connected, true ## flag: Account is completely initialized + IB::Connection.logger.info { "#{account.account} => Count of AccountValues: #{account.account_values.size}" } + else # unreasonable account_data received - request is still active + error "#{account.account} => Count of AccountValues too small: #{account.account_values.size}" , :reader + end + when IB::Messages::Incoming::PortfolioValue + account.contracts << msg.contract unless account.contracts.detect{|y| y.con_id == msg.contract.con_id } + add_or_update[account.portfolio_values,msg.portfolio_value] + IB::Connection.logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" } + end # case + end # account_data + end # subscribe + end # def + # safe access to account-data + def account_data account_or_id=nil + + if account_or_id.present? + account = account_or_id.is_a?(IB::Account) ? account_or_id : @accounts.detect{|x| x.account == account_or_id } + yield account + else + @accounts.map{|a| yield a} + end + + end + + alias activate_managed_accounts subscribe_account_updates + + + end + + class Connection + include ManagedAccounts + current.activate_managed_accounts! + rescue Workflow::NoTransitionAllowed => e + if current.workflow_state == :ready + current.disconnect! + resume + end + end +end diff --git a/plugins/ib/market-price.rb b/plugins/ib/market-price.rb new file mode 100644 index 0000000..d72d58c --- /dev/null +++ b/plugins/ib/market-price.rb @@ -0,0 +1,150 @@ +module IB + + module MarketPrice + # Ask for the Market-Price + # + # For valid contracts, either bid/ask or last_price and close_price are transmitted. + # + # If last_price is received, its returned. + # If not, midpoint (bid+ask/2) is used. Else the closing price will be returned. + # + # Any value (even 0.0) which is stored in IB::Contract.misc indicates that the contract is + # accepted by `request_market_data` and will be accepted by `place_order`, too. + # + # The result can be customized by a provided block. + # + # ```ruby + # IB::Symbols::Stocks.sie.market_price{ |x| x } + # -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3} + # ``` + # + # + # Raw-data are stored in the _bars_-property of IB::Contract + # (volatile, ie. data are not preserved when the Object is reused via Contract#merge) + # + # ```ruby + # u= (z1=IB::Stock.new(symbol: :ge)).market_price + # A: Requested market data is not subscribed. Displaying delayed market data. + # > u => 0.16975e3 + # > z1 => #"ge", (...) + # :currency => "USD", + # :exchange => "SMART" }, + # @bars = [ { last: -0.1e1, close: 0.16975e3, bid: -0.1e1, ask: -0.1e1 } ], + # @misc = { delayed: 0.16975e3 } + # + # ``` + # + # Fetching of market-data is a time consuming process. A threaded approach is suitable + # to get a bunch of market-data in time + # + # ```ruby + # th = (z2 = IB::Stock.new(symbol: :ge)).market_price(thread: true) + # th.join + # ``` + # assigns z2.misc with the value of the :last (or delayed_last) TickPrice-Message + # and returns the thread. + # + + def market_price delayed: true, thread: false, no_error: false + + tws= Connection.current # get the initialized ib-ruby instance + the_id , the_price = nil, nil + tickdata = Hash.new + q = Queue.new + # define requested tick-attributes + last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ], + [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]] + request_data_type = delayed ? :frozen_delayed : :frozen + + # From the tws-documentation (https://interactivebrokers.github.io/tws-api/market_data_type.html) + # Beginning in TWS v970, a IBApi.EClient.reqMarketDataType callback of 1 will occur automatically + # after invoking reqMktData if the user has live data permissions for the instrument. + # + # so - even if "delayed" is specified, realtime-data are returned if RT-permissions are present + # + + # method returns the (running) thread + th = Thread.new do + # about 11 sec after the request, the TWS returns :TickSnapshotEnd if no ticks are transmitted + # we don't have to implement our own timeout-criteria + s_id = tws.subscribe(:TickSnapshotEnd){|x| q.push(true) if x.ticker_id == the_id } + a_id = tws.subscribe(:Alert){|x| q.push(x) if [200, 354, 10167, 10168].include?( x.code ) && x.error_id == the_id } + # TWS Error 354: Requested market data is not subscribed. + + # subscribe to TickPrices + sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg| + [last,close,bid,ask].each do |x| + tickdata[x] = msg.the_data[:price] if x.include?( IB::TICK_TYPES[ msg.the_data[:tick_type]]) + # fast exit condition + q.push(true) if tickdata.size >= 4 + end if msg.ticker_id == the_id + end + # initialize »the_id« that is used to identify the received tick messages + # by firing the market data request + the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true + + while !q.closed? do + result = q.pop + if result.is_a? IB::Messages::Incoming::Alert + tws.logger.debug result.message + case result.code + when 200 + q.close + error "#{to_human} --> #{result.message}" unless no_error + when 354, # not subscribed to market data + 10167, + 10168 + if delayed && !(result.message =~ /market data is not available/) + tws.logger.debug "#{to_human} --> requesting delayed data" + tws.send_message :RequestMarketDataType, :market_data_type => 3 + self.misc = :delayed + sleep 0.1 + the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true + else + q.close + tws.logger.error "#{to_human} --> No marketdata permissions" unless no_error + end + end + elsif result.present? + q.close + tz = -> (z){ z.map{|y| y.to_s.split('_')}.flatten.count_duplicates.max_by{|k,v| v}.first.to_sym} + data = tickdata.map{|x,y| [tz[x],y]}.to_h + valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) } + self.bars << data # store raw data in bars + the_price = if block_given? + yield data + # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3} + else # behavior if no block is provided + if valid_data[data[:last]] + data[:last] + elsif valid_data[data[:bid]] + (data[:bid]+data[:ask])/2 + elsif data[:close].present? + data[:close] + else + nil + end + end + + self.misc = misc == :delayed ? { :delayed => the_price } : { realtime: the_price } + else + q.close + error "#{to_human} --> No Marketdata received " + end + end + + tws.unsubscribe sub_id, s_id, a_id + end + if thread + th # return thread + else + th.join + the_price # return + end + end # + end + class Contract + include MarketPrice + end +end diff --git a/plugins/ib/option-chain.rb b/plugins/ib/option-chain.rb new file mode 100644 index 0000000..c49c19f --- /dev/null +++ b/plugins/ib/option-chain.rb @@ -0,0 +1,167 @@ +module IB + + module OptionChain + + # returns the Option Chain (monthly options, expiry: third friday) + # of the contract (if available) + # + # + ## parameters + ### right:: :call, :put, :straddle ( default: :put ) + ### ref_price:: :request or a numeric value ( default: :request ) + ### sort:: :strike, :expiry + ### exchange:: List of Exchanges to be queried ( default: SMART) + ### trading_class ( optional ) + def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', trading_class: nil + + ib = Connection.current + + # binary interthread communication + finalize = Queue.new + + ## Enable Cashing of Definition-Matrix + @option_chain_definition ||= [] + + my_req = nil + + # ----------------------------------------------------------------------------------------------------- + # get OptionChainDefinition from IB ( instantiate cashed Hash ) + if @option_chain_definition.blank? + sub_sdop = ib.subscribe( :SecurityDefinitionOptionParameterEnd ) { |msg| finalize.push(true) if msg.request_id == my_req } + sub_ocd = ib.subscribe( :OptionChainDefinition ) do | msg | + if msg.request_id == my_req + message = msg.data + # transfer the first record to @option_chain_definition + if @option_chain_definition.blank? + @option_chain_definition = msg.data + end + # override @option_chain_definition if a decent combination of attributes is met + # us- options: use the smart dataset + # other options: prefer options of the default trading class +# if message[:exchange] == 'SMART' +# @option_chain_definition = msg.data +# finalize.push(true) +# end + if message[:trading_class] == symbol + @option_chain_definition = msg.data + finalize.push(true) + end + end + end + + c = verify.first # ensure a complete set of attributes + my_req = ib.send_message :RequestOptionChainDefinition, con_id: c.con_id, + symbol: c.symbol, + exchange: c.sec_type == :future ? c.exchange : "", # BOX,CBOE', + sec_type: c[:sec_type] + + finalize.pop # wait until data appeared + + ib.unsubscribe sub_sdop, sub_ocd + else + Connection.logger.info { "#{to_human} : using cached data" } + end + + # ----------------------------------------------------------------------------------------------------- + # select values and assign to options + # + unless @option_chain_definition.blank? + requested_strikes = if block_given? + ref_price = market_price if ref_price == :request + if ref_price.nil? + ref_price = @option_chain_definition[:strikes].min + + ( @option_chain_definition[:strikes].max - + @option_chain_definition[:strikes].min ) / 2 + Connection.logger.warn { "#{to_human} :: market price not set – using midpoint of available strikes instead: #{ref_price.to_f}" } + end + atm_strike = @option_chain_definition[:strikes].min_by { |x| (x - ref_price).abs } + the_grouped_strikes = @option_chain_definition[:strikes].group_by{|e| e <=> atm_strike} + begin + the_strikes = yield the_grouped_strikes + the_strikes.unshift atm_strike unless the_strikes.first == atm_strike # the first item is the atm-strike + the_strikes + rescue + Connection.logger.error "#{to_human} :: not enough strikes :#{@option_chain_definition[:strikes].map(&:to_f).join(',')} " + [] + end + else + @option_chain_definition[:strikes] + end + + # third Friday of a month + monthly_expirations = @option_chain_definition[:expirations].find_all {|y| (15..21).include? y.day } + # puts @option_chain_definition.inspect + option_prototype = -> ( ltd, strike ) do + IB::Option.new( symbol: symbol, + exchange: @option_chain_definition[:exchange], + trading_class: @option_chain_definition[:trading_class], + multiplier: @option_chain_definition[:multiplier], + currency: currency, + last_trading_day: ltd, + strike: strike, + right: right).verify &.first + end + options_by_expiry = -> ( schema ) do + # Array: [ yymm -> Options] prepares for the correct conversion to a Hash + Hash[ monthly_expirations.map do | l_t_d | + [ l_t_d.strftime('%y%m').to_i , schema.map { | strike | option_prototype[ l_t_d, strike ]}.compact ] + end ] # by Hash[ ] + end + options_by_strike = -> ( schema ) do + Hash[ schema.map do | strike | + [ strike , monthly_expirations.map { | l_t_d | option_prototype[ l_t_d, strike ]}.compact ] + end ] # by Hash[ ] + end + + if sort == :strike + options_by_strike[ requested_strikes ] + else + options_by_expiry[ requested_strikes ] + end + else + Connection.logger.error "#{to_human} ::No Options available" + nil # return_value + end + end # def + + # return a set of AtTheMoneyOptions + def atm_options ref_price: :request, right: :put, **params + option_chain( right: right, ref_price: ref_price, sort: :expiry, **params) do | chain | + chain[0] + end + + + end + + # return InTheMoneyOptions + def itm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: '' + option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain | + if right == :put + above_market_price_strikes = chain[1][0..count-1] + else + below_market_price_strikes = chain[-1][-count..-1].reverse + end # branch + end + end # def + + # return OutOfTheMoneyOptions + def otm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: '' + option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain | + if right == :put + # puts "Chain: #{chain}" + below_market_price_strikes = chain[-1][-count..-1].reverse + else + above_market_price_strikes = chain[1][0..count-1] + end + end + end + end # module + + Connection.current.activate_plugin 'verify' + Connection.current.activate_plugin 'market-price' + + class Contract + include OptionChain + end + +end # module diff --git a/plugins/ib/order-flow.rb b/plugins/ib/order-flow.rb new file mode 100644 index 0000000..3d598ac --- /dev/null +++ b/plugins/ib/order-flow.rb @@ -0,0 +1,157 @@ +module IB + +=begin + +Plugin to support a simple Order-Flow + +Public API +========== + +Extends IB::Order + +* check_margin + * depends on a previously submitted `what-if' order + * on success it returns the order-object for further processing, otherwise nil. +* place + * submit the order + * return a order-object for further processing +* modify + * modify price or quantity of the submitted order +* cancel + * submit a cancel request + +=end + module OrderFlow + # Placement + # + # The Order is only placed, if local_id is not set + # + # Modifies the Order-Object and returns the assigned local_id + def place + connection = IB::Connection.current + error "Unable to place order, next_local_id not known" unless connection.next_local_id + error "local_id present. Order is already placed. Do you want to modify?" unless local_id.nil? + # self.client_id = connection.client_id + self.local_id = connection.next_local_id + connection.next_local_id += 1 + self.placed_at = Time.now + #connection.place_order self.dup.then{|y| y.contract = nil; y}, contract + modify + end + + # Modify Order (convenience wrapper for send_message :PlaceOrder), returns order record received trom tws + def modify + error "Unable to modify order; local_id not specified" if local_id.nil? + error "Unable to place order, contract has to be specified" unless contract.is_a?( IB::Contract ) + + ib = IB::Connection.current + q = Queue.new + is = ib.subscribe( :OpenOrder ) do | msg | + puts msg.to_human + if msg.order.local_id == local_id + q << msg.order + end + end + ia = ib.subscribe( :Alert ) do | msg| + ib.logger.error msg.to_human + end + + self.modified_at = Time.now + ib.send_message :PlaceOrder, + :local_id => local_id, + :order => self.dup.then{|y| y.contract = nil; y}, + :contract => if contract.con_id.to_i > 0 + Contract.new con_id: the_contract.con_id, + exchange: the_contract.exchange + else + contract + end + + th = Thread.new{ sleep 1 ; q.close } + received_order = q.pop # synchronize + ib.unsubscribe ia, is + if q.closed? + error "order #{to_human} is not accepted", :reader + self # return original error after error handling + else + Thread.kill th + q.close + received_order + end + end + + # returns the order if the margin-requirements are met + # + # Details of the test are published in the log (level: LOGGER::INOT) + # + # typical setup + # ```ruby + # ib = IB::Connection.new + # ib.activate_plugin ... + # u = ib.clients.last + # submitted_order = u.preview( order: some_order, contract: some_contract ) + # .check_margin( 0.25 ) &.place + # place order only if after its placement Equity-with-loan ist minimal 25 percent higher then the margin requirements + # i.e. the margin utilization is max. 75 %. + # + # ``` + def check_margin treshold = 0.1 + error "Unable to check margin, forcast is not initialized" if order_state.nil? or order_state.init_margin_after.nil? + utilization = order_state.init_margin_after / order_state.equity_with_loan_after + if 1 - utilization > treshold + Connection.current.logger.info "Margin OK: #{action} #{total_quantity} of #{contract.to_human}: requirements: #{order_state.init_margin_change.round} #{contract.currency} equals to #{utilization.round(1) *100} % margin utilization" + self + else + Connection.current.logger.info "Margin requirements NOT met, utilization: #{utilization.round(2) *100} % ( margin requirement: #{order_state.init_margin_change.round} #{contract.currency})" + nil + end + end + # + # Auto Adjust implements a simple algorithm to ensure that an order is accepted + + # It reads `contract_detail.min_tick`. + # + # For min-tick smaller then 0.01, the value is rounded to the next higer digit. + # + # The method mutates the Order-Object. + # + # | min-tick | round | + # |--------------|------------| + # | 10 | 110 | + # | 1 | 111 | + # | 0.1 | 111.1 | + # | 0.01 | 111.11 | + # | 0.001 | 111.111 | + # | 0.0001 | 111.111 | + # |--------------|------------| + # + def auto_adjust + # lambda to perform the calculation + adjust_price = ->(a,b) do + count = -Math.log10(b).round.to_i + count = count -1 if count > 2 + a.round count + + end + + + error "No Contract provided to Auto adjust" unless contract.is_a? IB::Contract + + unless contract.is_a? IB::Bag + + min_tick = contract.then{ |y| y.contract_detail.is_a?( IB::ContractDetail ) ? y.contract_detail.min_tick : y.verify.first.contract_detail.min_tick } + # there are two attributes to consider: limit_price and aux_price + # limit_price + aux_price may be nil or an empty string. Then ".to_f.zero?" becomes true + self.limit_price = adjust_price.call(limit_price.to_d, min_tick) unless limit_price.to_f.zero? + self.aux_price = adjust_price.call(aux_price.to_d, min_tick) unless aux_price.to_f.zero? + end + end + + end # module OrderFlow + + class Order + include OrderFlow + end # class +# Connection.current.activate_plugin 'process-orders' +end # module IB + diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order-prototypes.rb new file mode 100644 index 0000000..faa4eef --- /dev/null +++ b/plugins/ib/order-prototypes.rb @@ -0,0 +1,118 @@ +module IB +=begin rdoc + +Plugin to build IB::Order objects through singletons + +Public API +========== + +Creates IB::Order objects + + +* IB::.order params +* IB::.summary +* IB::.parameters + +=end + + module OrderPrototype + + +#The Module OrderPrototypes provides a wrapper to define even complex ordertypes. +# +#The Order is build by +# +# IB::.order +# +#A description is available through +# +# puts IB::.summary +# +#Nessesary and optional arguments are printed by +# +# puts IB::.parameters +# +#Orders can be setup interactively +# +# > d = Discretionary.order +# Traceback (most recent call last): (..) +# IB::ArgumentError (IB::Discretionary.order -> A necessary field is missing: +# action: --> {"B"=>:buy, "S"=>:sell, "T"=>:short, "X"=>:short_exempt}) +# > d = Discretionary.order action: :buy +# IB::ArgumentError (IB::Discretionary.order -> A necessary field is missing: +# total_quantity: --> also aliased as :size) +# > d = Discretionary.order action: :buy, size: 100 +# Traceback (most recent call last): +# IB::ArgumentError (IB::Discretionary.order -> A necessary field is missing: limit_price: --> decimal) +# +# +# +#Prototypes are defined as module. They extend OrderPrototype and establish singleton methods, which +#can adress and extend similar methods from OrderPrototype. +# +# + + + + def order **fields + + # special treatment of size: positive numbers --> buy order, negative: sell + if fields[:size].present? && fields[:action].blank? + error "Size = 0 is not possible" if fields[:size].zero? + fields[:action] = fields[:size] >0 ? :buy : :sell + fields[:size] = fields[:size].abs + end + # change aliases to the original. We are modifying the fields-hash. + fields.keys.each{|x| fields[aliases.key(x)] = fields.delete(x) if aliases.has_value?(x)} + # inlcude defaults (arguments override defaults) + the_arguments = defaults.merge fields + # check if requirements are fullfilled + necessary = requirements.keys.detect{|y| the_arguments[y].nil?} + if necessary.present? + msg =self.name + ".order -> A necessary field is missing: #{necessary}: --> #{requirements[necessary]}" + error msg, :args, nil + end + if alternative_parameters.present? + unless ( alternative_parameters.keys & the_arguments.keys ).size == 1 + msg =self.name + ".order -> One of the alternative fields needs to be specified: \n\t:" + + "#{alternative_parameters.map{|x| x.join ' => '}.join(" or \n\t:")}" + error msg, :args, nil + end + end + + # initialise order with given attributes + IB::Order.new **the_arguments + end + + def alternative_parameters + {} + end + def requirements + { action: IB::VALUES[:side], total_quantity: 'also aliased as :size' } + end + + def defaults + { tif: :good_till_cancelled } + end + + def optional + { account: 'Account(number) to trade on' } + end + + def aliases + { total_quantity: :size } + end + + def parameters + the_output = ->(var){ var.map{|x| x.join(" --> ") }.join("\n\t: ")} + + "Required : " + the_output[requirements] + "\n --------------- \n" + + "Optional : " + the_output[optional] + "\n --------------- \n" + + end + + end +[ :forex, :market, :limit, :stop, :volatility, :premarket, :pegged, :combo, :adaptive ].each do | pt | + Connection.current.activate_plugin "order_prototypes/#{pt.to_s}" +end +end diff --git a/plugins/ib/order-prototypes/abstract.rb b/plugins/ib/order-prototypes/abstract.rb new file mode 100644 index 0000000..91f27a9 --- /dev/null +++ b/plugins/ib/order-prototypes/abstract.rb @@ -0,0 +1,67 @@ +# These modules are used to facilitate referencing of most common Ordertypes + +module IB + module OrderPrototype + + + + + def order **fields + + # special treatment of size: positive numbers --> buy order, negative: sell + if fields[:size].present? && fields[:action].blank? + error "Size = 0 is not possible" if fields[:size].zero? + fields[:action] = fields[:size] >0 ? :buy : :sell + fields[:size] = fields[:size].abs + end + # change aliases to the original. We are modifying the fields-hash. + fields.keys.each{|x| fields[aliases.key(x)] = fields.delete(x) if aliases.has_value?(x)} + # inlcude defaults (arguments override defaults) + the_arguments = defaults.merge fields + # check if requirements are fullfilled + necessary = requirements.keys.detect{|y| the_arguments[y].nil?} + if necessary.present? + msg =self.name + ".order -> A necessary field is missing: #{necessary}: --> #{requirements[necessary]}" + error msg, :args, nil + end + if alternative_parameters.present? + unless ( alternative_parameters.keys & the_arguments.keys ).size == 1 + msg =self.name + ".order -> One of the alternative fields needs to be specified: \n\t:" + + "#{alternative_parameters.map{|x| x.join ' => '}.join(" or \n\t:")}" + error msg, :args, nil + end + end + + # initialise order with given attributes + IB::Order.new the_arguments + end + + def alternative_parameters + {} + end + def requirements + { action: IB::VALUES[:side], total_quantity: 'also aliased as :size' } + end + + def defaults + { tif: :good_till_cancelled } + end + + def optional + { account: 'Account(number) to trade on' } + end + + def aliases + { total_quantity: :size } + end + + def parameters + the_output = ->(var){ var.map{|x| x.join(" --> ") }.join("\n\t: ")} + + "Required : " + the_output[requirements] + "\n --------------- \n" + + "Optional : " + the_output[optional] + "\n --------------- \n" + + end + + end +end diff --git a/plugins/ib/order-prototypes/adaptive.rb b/plugins/ib/order-prototypes/adaptive.rb new file mode 100644 index 0000000..e9574f3 --- /dev/null +++ b/plugins/ib/order-prototypes/adaptive.rb @@ -0,0 +1,40 @@ + +module IB +# module OrderPrototype + module Adaptive + extend OrderPrototype + class << self + + def defaults + Limit.defaults.merge algo_strategy: "Adaptive", + algo_params: { "adaptivePriority" => "Normal" } + end + + def aliases + Limit.aliases + end + + def requirements + Limit.requirements + end + + + def summary + <<-HERE + The Adaptive Algo combines IB’s Smart routing capabilities with user-defined + priority settings in an effort to achieve further cost efficiency at the + point of execution. Using the Adaptive algo leads to better execution prices + on average than for regular limit or market orders. + + Algo Strategy Value: Adaptive + + adaptivePriority: String. The ‘Priority’ selector determines the time taken + to scan for better execution prices. The ‘Urgent’ setting scans only briefly, + while the ‘Patient’ scan works more slowly and has a higher chance of + achieving a better overall fill for your order. Valid Value/Format: + Urgent > Normal > Patient + HERE + end + end + end +end diff --git a/plugins/ib/order-prototypes/all-in-one.rb b/plugins/ib/order-prototypes/all-in-one.rb new file mode 100644 index 0000000..937997f --- /dev/null +++ b/plugins/ib/order-prototypes/all-in-one.rb @@ -0,0 +1,46 @@ +module IB + +#Combo-Orders are used for NonGuaranteed Orders only. +#»Normal« Option-Spreads are transmited by ordinary Limit-Orders + module Combo + ### Basic Order Prototype: Combo with two limits + extend OrderPrototype + class << self + def defaults + ## todo implement serialisation of key/tag Hash to camelCased-keyValue-List +# super.merge order_type: :limit , combo_params: { non_guaranteed: true} + # for the time being, we use the array representation + super.merge order_type: :limit , combo_params: [ ['NonGuaranteed', true] ] + end + + + def requirements + Limit.requirements + end + + def aliases + Limit.aliases + end + + + def summary + <<-HERE + Create combination orders. It is constructed through options, stock and futures legs + (stock legs can be included if the order is routed through SmartRouting). + + Although a combination/spread order is constructed of separate legs, it is executed + as a single transaction if it is routed directly to an exchange. For combination orders + that are SmartRouted, each leg may be executed separately to ensure best execution. + + The »NonGuaranteed«-Flag is set to "false". A Pair of two securites should always be + routed »Guaranteed«, otherwise separate orders are prefered. + + If a Bag-Order with »NonGuarateed :true« should be submitted, the Order-Type would be + REL+MKT, LMT+MKT, or REL+LMT + -------- + Products: Options, Stocks, Futures + HERE + end # def + end # class + end # module combo +end # module ib diff --git a/plugins/ib/order-prototypes/combo.rb b/plugins/ib/order-prototypes/combo.rb new file mode 100644 index 0000000..937997f --- /dev/null +++ b/plugins/ib/order-prototypes/combo.rb @@ -0,0 +1,46 @@ +module IB + +#Combo-Orders are used for NonGuaranteed Orders only. +#»Normal« Option-Spreads are transmited by ordinary Limit-Orders + module Combo + ### Basic Order Prototype: Combo with two limits + extend OrderPrototype + class << self + def defaults + ## todo implement serialisation of key/tag Hash to camelCased-keyValue-List +# super.merge order_type: :limit , combo_params: { non_guaranteed: true} + # for the time being, we use the array representation + super.merge order_type: :limit , combo_params: [ ['NonGuaranteed', true] ] + end + + + def requirements + Limit.requirements + end + + def aliases + Limit.aliases + end + + + def summary + <<-HERE + Create combination orders. It is constructed through options, stock and futures legs + (stock legs can be included if the order is routed through SmartRouting). + + Although a combination/spread order is constructed of separate legs, it is executed + as a single transaction if it is routed directly to an exchange. For combination orders + that are SmartRouted, each leg may be executed separately to ensure best execution. + + The »NonGuaranteed«-Flag is set to "false". A Pair of two securites should always be + routed »Guaranteed«, otherwise separate orders are prefered. + + If a Bag-Order with »NonGuarateed :true« should be submitted, the Order-Type would be + REL+MKT, LMT+MKT, or REL+LMT + -------- + Products: Options, Stocks, Futures + HERE + end # def + end # class + end # module combo +end # module ib diff --git a/plugins/ib/order-prototypes/forex.rb b/plugins/ib/order-prototypes/forex.rb new file mode 100644 index 0000000..702fd27 --- /dev/null +++ b/plugins/ib/order-prototypes/forex.rb @@ -0,0 +1,40 @@ +module IB +# module UseOrder + module ForexLimit + extend OrderPrototype + class << self + + + def defaults + super.merge order_type: :limit , tif: :day + end + + + def requirements + super.merge cash_qty: '(true/false) to indicate to let IB calculate the cash-quantity of the alternate currency' + end + + + def summary + <<-HERE + Forex orders can be placed in denomination of second currency in pair using cashQty field. + Don't specify a limit-price to force immidiate execution. + HERE + end + end +=begin +2.5.0 :001 > f = Symbols::Forex[:eurusd] + => #"EUR", "exchange"=>"IDEALPRO", "currency"=>"USD", "sec_type"=>"CASH", "created_at"=>2018-01-20 05:21:01 +0100, "updated_at"=>2018-01-20 05:21:01 +0100, "con_id"=>0, "right"=>"", "include_expired"=>false}, @description="EURUSD"> + +2.5.0 :002 > uf = ForexLimit.order action: :buy, size: 15000, cash_qty: true +{:action=>:buy, :cash_qty=>true, :total_quantity=>15000} + => #"DAY", "order_type"=>"LMT", "side"=>"B", "cash_qty"=>true, "total_quantity"=>15000, "created_at"=>2018-01-20 05:21:06 +0100, "updated_at"=>2018-01-20 05:21:06 +0100, "active_start_time"=>"", "active_stop_time"=>"", "algo_strategy"=>"", "algo_id"=>"", "auction_strategy"=>0, "conditions"=>[], "continuous_update"=>0, "delta_neutral_designated_location"=>"", "delta_neutral_con_id"=>0, "delta_neutral_settling_firm"=>"", "delta_neutral_clearing_account"=>"", "delta_neutral_clearing_intent"=>"", "designated_location"=>"", "display_size"=>0, "discretionary_amount"=>0, "etrade_only"=>true, "exempt_code"=>-1, "ext_operator"=>"", "firm_quote_only"=>true, "not_held"=>false, "oca_type"=>0, "open_close"=>1, "opt_out_smart_routing"=>false, "origin"=>0, "outside_rth"=>false, "parent_id"=>0, "random_size"=>false, "random_price"=>false, "scale_auto_reset"=>false, "scale_random_percent"=>false, "scale_table"=>"", "short_sale_slot"=>0, "solicided"=>false, "transmit"=>true, "trigger_method"=>0, "what_if"=>false, "leg_prices"=>[], "algo_params"=>{}, "combo_params"=>[], "soft_dollar_tier_params"=>{"name"=>"", "val"=>"", "display_name"=>""}}, @order_states=[#"New", "filled"=>0, "remaining"=>0, "price"=>0, "average_price"=>0, "created_at"=>2018-01-20 05:21:06 +0100, "updated_at"=>2018-01-20 05:21:06 +0100}>]> + 2.5.0 :004 > C.place_order uf, f + => 4 + 2.5.0 :005 > 05:21:23.606 Got message 4 (IB::Messages::Incoming::Alert) + I, [2018-01-20T05:21:23.606819 #31020] INFO -- : TWS Warning 10164: Traders are responsible for understanding cash quantity details, which are provided on a best efforts basis only. + Restriction is specified in Precautionary Settings of Global Configuration/Presets. + +=end + end +end diff --git a/plugins/ib/order-prototypes/limit.rb b/plugins/ib/order-prototypes/limit.rb new file mode 100644 index 0000000..80a48fe --- /dev/null +++ b/plugins/ib/order-prototypes/limit.rb @@ -0,0 +1,193 @@ + +module IB + module Limit + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :limit + end + + def aliases + super.merge limit_price: :price + end + + def requirements + super.merge limit_price: "also aliased as :price" + end + + + def summary + <<-HERE + A Limit order is an order to buy or sell at a specified price or better. + The Limit order ensures that if the order fills, it will not fill at a price less favorable than + your limit price, but it does not guarantee a fill. + It appears in the orderbook. + HERE + end + end + end + module Discretionary + extend OrderPrototype + class << self + + def defaults + Limit.defaults + end + + def aliases + Limit.aliases.merge discretionary_amount: :dc + end + + def requirements + Limit.requirements + end + + def optional + super.merge discretionary_amount: :decimal + end + + def summary + <<-HERE + A Discretionary order is a Limitorder submitted with a hidden, + specified 'discretionary' amount off the limit price which may be used + to increase the price range over which the limit order is eligible to execute. + The market sees only the limit price. + The discretionary amount adds to the given limit price. The main effort is + to hide your real intentions from the public. Discretionary orders can be placed + for stocks and option on us exchanges, using 'SMART' routing. + HERE + end + + def example + <<-HERE + You want to by a stock for 65 $ or less. Its trading is volatile, the spread is large. + There is a technical resistence at 64.5 $ and you think, that there will be a price + floor somewhere between 64.5 and 65 $. You place a limit order + at 64.5 $, which appears in the order book. Secretly, you instruct your broker to + fill the order, if the price is lower then 65 $. Chances for a filling are inceased. + + ``` + dc_order = IB::Discretionary.order size: 1000, price: 65, dc: 0.5 + account.place order: dc_order, contract: IB::Stock.new( symbol: 'BN' ) + ``` + HERE + end + end + end +# module OrderPrototype + module Sweep2Fill + extend OrderPrototype + class << self + + def defaults + super.merge order_type: ':limit' , tif: :day, sweep_to_fill: true + end + + def aliases + Limit.aliases + end + + def requirements + Limit.requirements + end + + + def summary + <<-HERE + Sweep-to-fill orders are useful when a trader values speed of execution over price. A sweep-to-fill + order identifies the best price and the exact quantity offered/available at that price, and + transmits the corresponding portion of your order for immediate execution. Simultaneously it + identifies the next best price and quantity offered/available, and submits the matching quantity + of your order for immediate execution. + + ------------------------ + Products: CFD, STK, WAR (SMART only) + + HERE + end + end + end + module LimitIfTouched + extend OrderPrototype + class << self + + def defaults + Limit.defaults.merge order_type: :limit_if_touched + end + + def aliases + Limit.aliases.merge aux_price: :trigger_price + end + + def requirements + Limit.requirements.merge aux_price: 'also aliased as :trigger_price ' + end + + + def summary + <<-HERE + A Limit if Touched is an order to buy (or sell) a contract at a specified price or better, + below (or above) the market. This order is held in the system until the trigger price is touched. + An LIT order is similar to a stop limit order, except that an LIT sell order is placed above + the current market price, and a stop limit sell order is placed below. + HERE + end + end + end + + + module LimitOnClose + extend OrderPrototype + class << self + + def defaults + Limit.defaults.merge order_type: :limit_on_close + end + + def aliases + Limit.aliases + end + + def requirements + Limit.requirements + end + + + def summary + <<-HERE + A Limit-on-close (LOC) order will be submitted at the close and will execute if the + closing price is at or better than the submitted limit price. + HERE + end + end + end + + module LimitOnOpen + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :limit_on_open , tif: :opening_price + end + + def aliases + Limit.aliases + end + + def requirements + Limit.requirements + end + + + def summary + <<-HERE + A Limit-on-Open (LOO) order combines a limit order with the OPG time in force to create an + order that is submitted at the market's open, and that will only execute at the specified + limit price or better. Orders are filled in accordance with specific exchange rules. + HERE + end + end + end + # end +end diff --git a/plugins/ib/order-prototypes/market.rb b/plugins/ib/order-prototypes/market.rb new file mode 100644 index 0000000..075306a --- /dev/null +++ b/plugins/ib/order-prototypes/market.rb @@ -0,0 +1,116 @@ + +module IB +# module OrderPrototype + module Market + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'MKT' , tif: :day + end + + def aliases + super + end + + def requirements + super + end + + + def summary + <<-HERE + A Market order is an order to buy or sell at the market bid or offer price. + A market order may increase the likelihood of a fill and the speed of execution, + but unlike the Limit order a Market order provides no price protection and + may fill at a price far lower/higher than the current displayed bid/ask. + HERE + end + end + end + module MarketIfTouched + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'MIT' , tif: :day + end + + def aliases + super + end + + def requirements + super + end + + + def summary + <<-HERE + A Market if Touched (MIT) is an order to buy (or sell) a contract below (or above) the market. + Its purpose is to take advantage of sudden or unexpected changes in share or other prices and + rovides investors with a trigger price to set an order in motion. + Investors may be waiting for excessive strength (or weakness) to cease, which might be represented + by a specific price point. + MIT orders can be used to determine whether or not to enter the market once a specific price level + has been achieved. This order is held in the system until the trigger price is touched, and + is then submitted as a market order. An MIT order is similar to a stop order, except that an MIT + sell order is placed above the current market price, and a stop sell order is placed below. + HERE + end + end + end + + + module MarketOnClose + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'MOC' , tif: :day + end + + def aliases + super + end + + def requirements + super + end + + + def summary + <<-HERE + A Market-on-Close (MOC) order is a market order that is submitted to execute as close + to the closing price as possible. + HERE + end + end + end + + module MarketOnOpen + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'MOC' , tif: :opening_price + end + + def aliases + super + end + + def requirements + super + end + + + def summary + <<-HERE + A Market-on-Close (MOC) order is a market order that is submitted to execute as close + to the closing price as possible. + HERE + end + end + end +end diff --git a/plugins/ib/order-prototypes/pegged.rb b/plugins/ib/order-prototypes/pegged.rb new file mode 100644 index 0000000..50390e2 --- /dev/null +++ b/plugins/ib/order-prototypes/pegged.rb @@ -0,0 +1,169 @@ +module IB + module Pegged2Primary + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :pegged_to_primary , tif: :day + end + + def aliases + super.merge limit_price: :price_cap, + aux_price: :offset_amount + end + + def requirements + super.merge aux_price: 'also aliased as :offset_amount', + limit_price: 'aliased as :price_cap' + end + + def optional + super + end + + def summary + <<-HERE + Relative (a.k.a. Pegged-to-Primary) orders provide a means for traders + to seek a more aggressive price than the National Best Bid and Offer + (NBBO). By acting as liquidity providers, and placing more aggressive + bids and offers than the current best bids and offers, traders increase + their odds of filling their order. Quotes are automatically adjusted as + the markets move, to remain aggressive. For a buy order, your bid is + pegged to the NBB by a more aggressive offset, and if the NBB moves up, + your bid will also move up. If the NBB moves down, there will be no + adjustment because your bid will become even more aggressive and + execute. For sales, your offer is pegged to the NBO by a more + aggressive offset, and if the NBO moves down, your offer will also move + down. If the NBO moves up, there will be no adjustment because your + offer will become more aggressive and execute. In addition to the + offset, you can define an absolute cap, which works like a limit price, + and will prevent your order from being executed above or below a + specified level. + Supported Products: Stocks, Options and Futures + ------ + not available on paper trading + HERE + end + end + end + module Pegged2Market + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'PEG MKT' , tif: :day + end + + def aliases + Limit.aliases.merge aux_price: :market_offset + end + + def requirements + super.merge aux_price: :decimal + end + + def optional + super + end + + def summary + <<-HERE + A pegged-to-market order is designed to maintain a purchase price relative to the + national best offer (NBO) or a sale price relative to the national best bid (NBB). + Depending on the width of the quote, this order may be passive or aggressive. + The trader creates the order by entering a limit price which defines the worst limit + price that they are willing to accept. + Next, the trader enters an offset amount which computes the active limit price as follows: + Sell order price = Bid price + offset amount + Buy order price = Ask price - offset amount + HERE + end + end + end + + module Pegged2Stock + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'PEG STK', tif: :day + end + + def requirements + super.merge total_quantity: :decimal, + delta: 'required Delta of the Option', + starting_price: 'initial Limit-Price for the Option' + end + + def optional + super.merge stock_ref_price: 'Stock Reference Price', + stock_range_lower: 'Lowest acceptable Stock Price', + stock_range_upper: 'Highest accepable Stock Price' + end + + def summary + <<-HERE + Options ONLY + ------------ + A Pegged to Stock order continually adjusts the option order price by the product of a signed user- + defined delta and the change of the option's underlying stock price. + The delta is entered as an absolute and assumed to be positive for calls and negative for puts. + A buy or sell call order price is determined by adding the delta times a change in an underlying stock + price to a specified starting price for the call. + To determine the change in price, the stock reference price is subtracted from the current NBBO + midpoint. The Stock Reference Price can be defined by the user, or defaults to the + the NBBO midpoint at the time of the order if no reference price is entered. + You may also enter a high/low stock price range which cancels the order when reached. The + delta times the change in stock price will be rounded to the nearest penny in favor of the order. + ------------ + HERE + end + end + end + + module Pegged2Benchmark + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :pegged_to_benchmark, + is_pegged_change_amount_decrease: false, + tif: :day + end + + def requirements + super.merge total_quantity: :decimal, + starting_price: 'initial Limit-Price for the contract to trade' , + pegged_change_amount: ' (increase/decrease) by... (and likewise for price moving in opposite direction)', + reference_change_amount: ' ... whenever there is a price change of...', + reference_contract_id: 'the conid of the reference contract' + end + + def aliases + super.merge reference_change_amount: :reference_change_by, + pegged_change_amount: :change_by, + is_pegged_change_amount_decrease: :decrease, + reference_contract_id: :reference + + end + + + def optional + super.merge stock_ref_price: 'starting price of the reference contract', + stock_range_lower: 'Lowest acceptable Price of the reference contract', + stock_range_upper: 'Highest accepable Price of the reference contract', + is_pegged_change_amount_decrease: 'increase(true) / decrease(false) Price (default: false)', + reference_exchange_id: "Exchange of the reference contract" + end + + def summary + <<-HERE + The Pegged to Benchmark order is similar to the Pegged to Stock order for options, + except that the Pegged to Benchmark allows you to specify any asset type as the + reference (benchmark) contract for a stock or option order. Both the primary and + reference contracts must use the same currency. + HERE + end + end + end +end diff --git a/plugins/ib/order-prototypes/premarket.rb b/plugins/ib/order-prototypes/premarket.rb new file mode 100644 index 0000000..6c147cc --- /dev/null +++ b/plugins/ib/order-prototypes/premarket.rb @@ -0,0 +1,31 @@ +module IB +# module OrderPrototype + module AtAuction + extend OrderPrototype + class << self + + def defaults + { order_type: 'MTL' , tif: "AUC"} + end + + def aliases + super.merge limit_price: :price + end + + def requirements + super.merge limit_price: :decimal + end + + + def summary + <<-HERE + An auction order is entered into the electronic trading system during the pre-market + opening period for execution at the Calculated Opening Price (COP). + If your order is not filled on the open, the order is re-submitted as a + limit order with the limit price set to the COP or the best bid/ask after the market opens. + Products: FUT, STK + HERE + end + end + end +end diff --git a/plugins/ib/order-prototypes/stop.rb b/plugins/ib/order-prototypes/stop.rb new file mode 100644 index 0000000..a53dfcf --- /dev/null +++ b/plugins/ib/order-prototypes/stop.rb @@ -0,0 +1,202 @@ + +module IB + module SimpleStop + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :stop + end + + def aliases + super.merge aux_price: :price + end + + def requirements + super.merge aux_price: 'Price where the action is triggert. Aliased as :price' + end + + + def summary + <<-HERE + A Stop order is an instruction to submit a buy or sell market order if and when the + user-specified stop trigger price is attained or penetrated. A Stop order is not guaranteed + a specific execution price and may execute significantly away from its stop price. + + A Sell Stop order is always placed below the current market price and is typically used + to limit a loss or protect a profit on a long stock position. + + A Buy Stop order is always placed above the current market price. It is typically used + to limit a loss or help protect a profit on a short sale. + HERE + end + end + end + module StopLimit + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :stop_limit + end + + def aliases + Limit.aliases.merge aux_price: :stop_price + end + + def requirements + Limit.requirements.merge aux_price: 'Price where the action is triggert. Aliased as :stop_price' + end + + + def summary + <<-HERE + A Stop-Limit order is an instruction to submit a buy or sell limit order when + the user-specified stop trigger price is attained or penetrated. The order has + two basic components: the stop price and the limit price. When a trade has occurred + at or through the stop price, the order becomes executable and enters the market + as a limit order, which is an order to buy or sell at a specified price or better. + HERE + end + end + end + module StopProtected + extend OrderPrototype + class << self + + def defaults + super.merge order_type: :stop_protected + end + + def aliases + SimpleStop.aliases + end + + def requirements + SimpleStop.requirements + end + + + def summary + <<-HERE + US-Futures only + ---------------------------- + A Stop with Protection order combines the functionality of a stop limit order + with a market with protection order. The order is set to trigger at a specified + stop price. When the stop price is penetrated, the order is triggered as a + market with protection order, which means that it will fill within a specified + protected price range equal to the trigger price +/- the exchange-defined protection + point range. Any portion of the order that does not fill within this protected + range is submitted as a limit order at the exchange-defined trigger price +/- + the protection points. + HERE + end + end + end +# module OrderPrototype + module TrailingStop + extend OrderPrototype + class << self + + + def defaults + super.merge order_type: :trailing_stop , tif: :day + end + + def aliases + super.merge trail_stop_price: :price, + aux_price: :trailing_amount + end + + def requirements + ## usualy the trail_stop_price is the market-price minus(plus) the trailing_amount + super.merge trail_stop_price: 'Price to trigger the action, aliased as :price' + + end + + def alternative_parameters + { aux_price: 'Trailing distance in absolute terms, aliased as :trailing_amount', + trailing_percent: 'Trailing distance in relative terms'} + end + + def summary + <<-HERE + A "Sell" trailing stop order sets the stop price at a fixed amount below the market + price with an attached "trailing" amount. As the market price rises, the stop price + rises by the trail amount, but if the stock price falls, the stop loss price doesn't + change, and a market order is submitted when the stop price is hit. This technique + is designed to allow an investor to specify a limit on the maximum possible loss, + without setting a limit on the maximum possible gain. + + "Buy" trailing stop orders are the mirror image of sell trailing stop orders, and + are most appropriate for use in falling markets. + + Note that Trailing Stop orders can have the trailing amount specified as a percent, + or as an absolute amount which is specified in the auxPrice field. + + HERE + end # summary + end # class self + end # module + + module TrailingStopLimit + extend OrderPrototype + class << self + + + def defaults + super.merge order_type: :trailing_limit , tif: :day + end + + def aliases + Limit.aliases + end + + def requirements + super.merge trail_stop_price: 'Price to trigger the action', + limit_price_offset: 'a pRICE' + + end + + def alternative_parameters + { aux_price: 'Trailing distance in absolute terms', + trailing_percent: 'Trailing distance in relative terms'} + end + + def summary + <<-HERE + A trailing stop limit order is designed to allow an investor to specify a + limit on the maximum possible loss, without setting a limit on the maximum + possible gain. A SELL trailing stop limit moves with the market price, and + continually recalculates the stop trigger price at a fixed amount below + the market price, based on the user-defined "trailing" amount. The limit + order price is also continually recalculated based on the limit offset. As + the market price rises, both the stop price and the limit price rise by + the trail amount and limit offset respectively, but if the stock price + falls, the stop price remains unchanged, and when the stop price is hit a + limit order is submitted at the last calculated limit price. A "Buy" + trailing stop limit order is the mirror image of a sell trailing stop + limit, and is generally used in falling markets. + + Products: BOND, CFD, CASH, FUT, FOP, OPT, STK, WAR + HERE + end + end + +# def TrailingStopLimit(action:str, quantity:float, lmtPriceOffset:float, +# trailingAmount:float, trailStopPrice:float): +# +# # ! [trailingstoplimit] +# order = Order() +# order.action = action +# order.orderType = "TRAIL LIMIT" +# order.totalQuantity = quantity +# order.trailStopPrice = trailStopPrice +# order.lmtPriceOffset = lmtPriceOffset +# order.auxPrice = trailingAmount +# # ! [trailingstoplimit] +# return order +# +# + end +end diff --git a/plugins/ib/order-prototypes/volatility.rb b/plugins/ib/order-prototypes/volatility.rb new file mode 100644 index 0000000..a5caa73 --- /dev/null +++ b/plugins/ib/order-prototypes/volatility.rb @@ -0,0 +1,39 @@ +module IB +# module OrderPrototype + module Volatility + ### todo : check again. Order is siently accepted, but not acknowledged + extend OrderPrototype + class << self + + def defaults + { order_type: :volatility, volatility_type: 2 } #default is annual volatility + end + + + def requirements + super.merge volatility: "the desired Option implied Vola (in %)" + end + + def aliases + super.merge volatility: :volatility_percent + end + + def summary + <<-HERE + Investors are able to create and enter Volatility-type orders for options and combinations + rather than price orders. Option traders may wish to trade and position for movements in the + price of the option determined by its implied volatility. Because implied volatility is a key + determinant of the premium on an option, traders position in specific contract months in an + effort to take advantage of perceived changes in implied volatility arising before, during or + after earnings or when company specific or broad market volatility is predicted to change. + In order to create a Volatility order, clients must first create a Volatility Trader page from + the Trading Tools menu and as they enter option contracts, premiums will display in percentage + terms rather than premium. The buy/sell process is the same as for regular orders priced in + premium terms except that the client can limit the volatility level they are willing to pay or receive. + -------- + Products: FOP, OPT + HERE + end + end + end +end diff --git a/plugins/ib/probability-of-expiring.rb b/plugins/ib/probability-of-expiring.rb new file mode 100644 index 0000000..f36717a --- /dev/null +++ b/plugins/ib/probability-of-expiring.rb @@ -0,0 +1,109 @@ +module IB + module ProbabilityOfExpiring + + # Use by calling + # a = Stock.new symbol: 'A' + # + require 'prime' + require 'distribution' + + + + def probability_of_assignment **args + ( probability_of_expiring(**args) - 1 ).abs + end + def probability_of_expiring **args + @probability_of_expiring = calculate_probability_of_expiring(**args) if @probability_of_expiring.nil? || ! args.empty? + @probability_of_expiring + end + + private +=begin +Here are the steps to calculate the probability of expiry cone for a stock in +the next six months using the Black-Scholes model: + +* Determine the current stock price and the strike price for the option you + are interested in. Let's say the current stock price is $100 and the strike + price is $110. * Determine the time to expiry. In this case, we are + interested in the next six months, so the time to expiry is 0.5 years. * + Determine the implied volatility of the stock. Implied volatility is a measure + of the expected volatility of the stock over the life of the option, and can be + estimated from the option prices in the market. + +* Use the Black-Scholes formula to calculate the probability of the stock + expiring within the range of prices that make up the expiry cone. The formula + is: + + P = N(d2) + + Where P is the probability of the stock expiring within the expiry cone, and + N is the cumulative distribution function of the standard normal + distribution. d2 is calculated as: + + d2 = (ln(S/K) + (r - 0.5 * σ^2) * T) / (σ * sqrt(T)) + + Where S is the current stock price, K is the strike price, r is the risk-free + interest rate, σ is the implied volatility, and T is the time to expiry. + + Look up the value of N(d2) in a standard normal distribution table, or use a + calculator or spreadsheet program that can calculate cumulative distribution + functions. + + The result is the probability of the stock expiring within the expiry cone. + For example, if N(d2) is 0.35, then the probability of the stock expiring + within the expiry cone is 35%. + +(ChatGPT) +=end + def calculate_probability_of_expiring price: nil, + interest: 0.03, + iv: nil, + strike: nil, + expiry: nil, + ref_date: Date.today + + if iv.nil? && self.respond_to?( :greek ) + IB::Connection.logger.info "Probability_of_expiring: using current IV and Underlying-Price for calculation" + request_greeks if greek.nil? + iv = greek.implied_volatility + price = greek.under_price if price.nil? + end + error "ProbabilityOfExpiringCone needs iv as input" if iv.nil? || iv.zero? + + if price.nil? + price = if self.strike.to_i.zero? + market_price + else + underlying.market_price + end + end + error "ProbabilityOfExpiringCone needs price as input" if price.to_i.zero? + + + strike ||= self.strike + error "ProbabilityOfExpiringCone needs strike as input" if strike.to_i.zero? + + if expiry.nil? + if !last_trading_day.present? || last_trading_day.empty? + error "ProbabilityOfExpiringCone needs expiry as input" + else + expiry = last_trading_day + end + end + time_to_expiry = ( Date.parse( expiry.to_s ) - ref_date ).to_i + + # # Calculate d1 and d2 + d1 = (Math.log(price/strike.to_f) + (interest + 0.5*iv**2)*time_to_expiry) / (iv * Math.sqrt(time_to_expiry)) + d2 = d1 - iv * Math.sqrt(time_to_expiry) + # + # # Calculate the probability of expiry cone + Distribution::Normal.cdf(d2) + + end + end + + class Contract + include ProbabilityOfExpiring + end + +end diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb new file mode 100644 index 0000000..2cbdd71 --- /dev/null +++ b/plugins/ib/process-orders.rb @@ -0,0 +1,155 @@ +module IB +=begin + +Plugin for a comfortable processing of orders + +Public API +========== + +Extends IB::Connection + +* initialize_order_handling + + subscribes to various tws-messages and keeps record of the order-state + +* request_open_orders + + (aliased as UpdateOrders) erases account.orders and requests open-orders from the TWS + and populates Account#Orders + +=end + module ProcessOrders + protected + def initialize_order_handling + + subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg| + case msg + + when IB::Messages::Incoming::CommissionReport + # Commission-Reports are not assigned to a order - + logger.info "CommissionReport -------#{msg.exec_id} :...:C: #{msg.commission} :...:P/L: #{msg.realized_pnl}-" + when IB::Messages::Incoming::OrderStatus + + # The order-state only links via local_id and perm_id to orders. + # There is no reference to a contract or an account + + success = update_order_dependent_object( msg.order_state) do |o| + o.order_states.save_insert msg.order_state, :status + end + + logger.warn { "Order State not assigned-- #{msg.order_state.to_human} ----------" } if success.nil? + + when IB::Messages::Incoming::OpenOrder + account_data(msg.order.account) do | this_account | + # first update the contracts + # make open order equal to IB::Spreads (include negativ con_id) + msg.contract[:con_id] = -msg.contract.combo_legs.map{|y| y.con_id}.sum if msg.contract.is_a? IB::Bag + msg.contract.orders.save_insert msg.order, :local_id + this_account.contracts.save_insert msg.contract, :con_id, false + # now save the order-record + msg.order.contract = msg.contract + this_account.orders.save_insert msg.order, :local_id + end + + # update_ib_order msg ## aus support + when IB::Messages::Incoming::OpenOrderEnd + # exitcondition=true + logger.debug { "OpenOrderEnd" } + + when IB::Messages::Incoming::ExecutionData + # Excution-Data are fired independly from order-states. + # The Objects are stored at the associated order + success = update_order_dependent_object( msg.execution) do |o| + o.executions << msg.execution + if msg.execution.cumulative_quantity.to_i == o.total_quantity.abs + logger.info{ "#{o.account} --> #{o.contract.symbol}: Execution completed" } + o.order_states << IB::OrderState.new( perm_id: o.perm_id, + local_id: o.local_id, + status: 'Filled' ) + # update portfoliovalue + a = @accounts.detect{ | x | x.account == o.account } # we are in a mutex controlled environment + pv = a.portfolio_values.detect{ | y | y.contract.con_id == o.contract.con_id} + change = o.action == :sell ? -o.total_quantity : o.total_quantity + if pv.present? + pv.update_attribute :position, pv.position + change + else + a.portfolio_values << IB::PortfolioValue.new( position: change, contract: o.contract ) + end + else + logger.debug{ "#{o.account} --> #{o.contract.symbol}: Execution not completed (#{msg.execution.cumulative_quantity.to_i}/#{o.total_quantity.abs})" } + end # branch + end # block + + logger.warn { "Execution-Record not assigned-- #{msg.execution.to_human} ----------" } if success.nil? + + end # case msg.code + end # do + end # def subscribe + + # Resets the order-array for each account. + # Requests all open (eg. pending) orders from the tws + # + # Waits until the OpenOrderEnd-Message is received + + public + def request_open_orders + + q = Queue.new + subscription = subscribe( :OpenOrderEnd ) { q.push(true) } # signal success + account_data {| account | account.orders = [] } + send_message :RequestAllOpenOrders + ## the OpenOrderEnd-message usually appears after 0.1 sec. + ## we wait for 1 sec. + th = Thread.new{ sleep 1 ; q.close } + + q.pop # wait for OpenOrderEnd or finishing of thread + + unsubscribe subscription + if q.closed? + 5.times do + logger.fatal { "Is the API in read-only modus? No Open Order Message received! "} + sleep 0.2 + end + else + Thread.kill(th) + q.close + account_data {| account | account.orders } # reset order array + end + end + + alias update_orders request_open_orders + + private +=begin +UpdateOrderDependingObject + +Generic method which enables operations on the order-Object, +which is associated to OrderState-, Execution-, CommissionReport- +events fired by the tws. +The order is identified by local_id and perm_id + +Everything is carried out in a mutex-synchonized environment +=end + def update_order_dependent_object order_dependent_object # :nodoc: + account_data do | a | + order = if order_dependent_object.local_id.present? + a.locate_order local_id: order_dependent_object.local_id + else + a.locate_order perm_id: order_dependent_object.perm_id + end + yield order if order.present? + end + end + + +end # module + +class Connection + include ProcessOrders +end +Connection.current.activate_plugin 'managed-accounts' +Connection.current.initialize_managed_accounts! +Connection.current.initialize_order_handling! + +end ## module IB + diff --git a/plugins/ib/roll.rb b/plugins/ib/roll.rb new file mode 100644 index 0000000..e28d502 --- /dev/null +++ b/plugins/ib/roll.rb @@ -0,0 +1,86 @@ +module IB + module RollFuture + # helper method to roll an existing future + # + # Argument is the expiry of the target-future or the distance + # + # > nq = IB::Symbols::Futures.nq.verify.first + # > t= nq.roll to: '3m' + # > puts t.as_table +# ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +# │ Roll NQ future from Sep 24 to Dec 24 / buy 1 / sell 1 t= nq.roll expiry: 202412 + # > puts t.to_human + # / sell 1 + + + def roll **args + print_expiry = ->(f){ Date.parse(f.last_trading_day).strftime('%b %y') } + error "specify expiry to roll a future" if args.empty? + args[:to] = args[:expiry] if args[:expiry].present? && args[:expiry].to_s =~ /[mwMW]$/ + args[:expiry]= IB::Spread.transform_distance( expiry, args.delete(:to )) if args[:to].present? + + new_future = merge( **args ).verify.first + error "Cannot roll future; target is no IB::Contract" unless new_future.is_a? IB::Future + target = IB::Spread.new exchange: exchange, symbol: symbol, currency: currency, + description: " " rolling to " + # r.combo_legs.to_human + # => ["", ""] + # + # rolls the Option to another strike and/or expiry + # + # Same Expiry, roll down the strike + # `r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( strike: 1900 ) ` + # + # Same Expiry, roll to the next month + # `r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( expiry: '+1m' ) ` + + def roll **args + error "specify strike and expiry to roll option" if args.empty? + args[:to] = args[:expiry] if args[:expiry].present? && args[:expiry].to_s =~ /[mwMW]$/ + args[:expiry]= IB::Spread.transform_distance( expiry, args.delete(:to )) if args[:to].present? + + new_option = merge( ** args ).then{ | y | y.next_expiry{ y.expiry } } + + myself = con_id.to_i.zero? ? self.verify.first : self + error "Cannot roll option; target is no IB::Contract" unless new_option.is_a? IB::Option + error "Cannot roll option; Option cannot be verified" unless myself.is_a? IB::Option + target = IB::Spread.new exchange: exchange, symbol: symbol, currency: currency + target.add_leg myself, action: :buy + target.add_leg new_option, action: :sell + target.description= target.description.sub(/added (var){ var.empty? ? "none" : var.map{|x| x.join(" --> ") }.join("\n\t: ")} + + "Required : " + the_output[requirements] + "\n --------------- \n" + + "Optional : " + the_output[optional] + "\n --------------- \n" + + end + end + Connection.current.activate_plugin "verify" + [:straddle, :strangle, :vertical, :calendar, :"stock-spread", :butterfly].each do | pt | + Connection.current.activate_plugin "spread_prototypes/#{pt.to_s}" + end +end + diff --git a/plugins/ib/spread-prototypes/butterfly.rb b/plugins/ib/spread-prototypes/butterfly.rb new file mode 100644 index 0000000..efede8a --- /dev/null +++ b/plugins/ib/spread-prototypes/butterfly.rb @@ -0,0 +1,77 @@ +module IB + + module Butterfly + + extend SpreadPrototype + class << self + + # Fabricate a Butterfly from Scratch + # ----------------------------------------- + # + # + # + # Call with + # IB::Butterfly.fabricate IB::Option.new( symbol: :estx50, strike: 3000, expiry:'201901'), + # front: 2850, back: 3150 + # + # or + # IB::Butterfly.build from: Symbols::Index.stoxx + # strike: 3000 + # expiry: '201901', front: 2850, back: 3150 + # + # where :strike defines the center of the Spread. + def fabricate master, front:, back: + + error "fabrication is based on a master option. Please specify as first argument" unless master.is_a?(IB::Option) + strike = master.strike + master.right = :put unless master.right == :call + l= master.verify + if l.empty? + error "Invalid Parameters. No Contract found #{master.to_human}" + elsif l.size > 1 + error "ambigous contract-specification: #{l.map(&:to_human).join(';')}" + available_trading_classes = l.map( &:trading_class ).uniq + if available_trading_classes.size >1 + error "Refine Specification with trading_class: #{available_trading_classes.join('; ')} " + else + error "Respecify expiry, verification reveals #{l.size} contracts (only 1 is allowed)" + end + end + + initialize_spread( master ) do | the_spread | + strikes = [front, master.strike, back] + strikes.zip([1, -2, 1]).each do |strike, ratio| + action = ratio >0 ? :buy : :sell + leg = IB::Option.new( master.attributes.merge( strike: strike )).verify.first.essential + the_spread.add_leg leg, action: action, ratio: ratio.abs + end + the_spread.description = the_description( the_spread ) + the_spread.symbol = master.symbol + end + end + + def build from: , front:, back:, **options + underlying_attributes = { expiry: IB::Future.next_expiry, right: :put }.merge( from.attributes.slice( :symbol, :currency, :exchange, :strike )).merge( options ) + fabricate IB::Option.new( underlying_attributes), front: front, back: back + end + + def the_description spread + x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:strike )].transpose + "" + end + + def defaults + super.merge expiry: IB::Future.next_expiry, + right: :put + end + + + def requirements + super.merge back: "the strike of the lower bougth option", + front: "the strike of the upper bougth option" + + end + + end # class + end # module +end # module ib diff --git a/plugins/ib/spread-prototypes/calendar.rb b/plugins/ib/spread-prototypes/calendar.rb new file mode 100644 index 0000000..78971a0 --- /dev/null +++ b/plugins/ib/spread-prototypes/calendar.rb @@ -0,0 +1,97 @@ +module IB + + module Calendar + + extend SpreadPrototype + class << self + + +# Fabricate a Calendar-Spread from a Master-Option +# ----------------------------------------- +# If one Leg is known, the other is build through replacing the expiry +# The second leg is always SOLD ! +# +# Call with +# IB::Calendar.fabricate an_option, the_other_expiry + def fabricate master, the_other_expiry + + error "Argument must be a IB::Future or IB::Option" unless [:option, :future_option, :future ].include? master.sec_type + m = master.verify.first + error "Argument is a #{master.class}, but Verification failed" unless m.is_a? IB::Contract + the_other_expiry = the_other_expiry.values.first if the_other_expiry.is_a?(Hash) + back = IB::Spread.transform_distance m.expiry, the_other_expiry + the_other_contract = m.merge( expiry: back ).verify.first + error "Verification of second leg failed" unless the_other_contract.is_a? IB::Contract + target = IB::Spread.new exchange: m.exchange, symbol: m.symbol, currency: m.currency + target.add_leg m, action: :buy + target.add_leg the_other_contract, action: :sell + +# calendar = m.roll expiry: back + error "Initialisation of Legs failed" if target.legs.size != 2 + target.description = the_description( target ) + target # return fabricated spread + end + + +# Build Vertical out of an Underlying +# ----------------------------------------- +# Needed attributes: :strike, :expiry( front: expiry1, back: expiry2 ), right +# +# Optional: :trading_class, :multiplier +# +# Call with +# IB::Calendar.build from: IB::Contract, front: an_expiry, back: an_expiry, +# right: {put or call}, strike: a_strike + def build from:, front: nil, back: nil, right: :put, strike: nil, **fields + underlying = if from.is_a? IB::Option + right ||= from.right + front ||= from.expiry + strike ||= from.strike + details = from.verify.first.contract_detail + IB::Contract.new( con_id: details.under_con_id, + currency: from.currency).verify.first.essential + else + error "missing essential parameter: `strike`" unless strike.present? + from + end + error "`front:` and `back:` expiries are required" unless front.present? && back.present? + kind = { :front => front, :back => back } + initialize_spread( underlying ) do | the_spread | + leg_prototype = IB::Option.new underlying.invariant_attributes.except( :sec_type ) + .slice( :currency, :symbol, :exchange ) + .merge(defaults) + .merge( fields ) + .merge( strike: strike ) + kind[:back] = IB::Spread.transform_distance front, back + leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future) + leg1 = leg_prototype.merge( expiry: kind[:front] ).verify.first + leg2 = leg_prototype.merge( expiry: kind[:back] ).verify.first + unless leg2.is_a? IB::Option + leg2_trading_class = '' + leg2 = leg_prototype.merge( expiry: kind[:back] ).verify.first + end + the_spread.add_leg leg1 , action: :buy + the_spread.add_leg leg2 , action: :sell + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) rescue nil + end + end + + def defaults + super.merge right: :put +# expiry: IB::Future.next_expiry, + end + + + def the_description spread + x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:last_trading_day )].transpose + f_or_o = if spread.legs.first.is_a?(IB::Future) + "Future" + else + "#{spread.legs.first.right}(#{spread.legs.first.strike})" + end + "" + end + end # class + end # module vertical +end # module ib diff --git a/plugins/ib/spread-prototypes/stock-spread.rb b/plugins/ib/spread-prototypes/stock-spread.rb new file mode 100644 index 0000000..b904094 --- /dev/null +++ b/plugins/ib/spread-prototypes/stock-spread.rb @@ -0,0 +1,56 @@ +module IB + + module StockSpread + extend SpreadPrototype + class << self + + # Fabricate a StockSpread from Scratch + # ----------------------------------------- + # + # + # + # Call with + # IB::StockSpread.fabricate 'GE','F', ratio:[1,-2] + # + # or + # IB::StockSpread.fabricate IB::Stock.new(symbol:'GE'), 'F', ratio:[1,-2] + # + # + # + def fabricate *underlying, ratio: [1,-1], **args + # + are_stocks = ->(l){ l.all?{|y| y.is_a? IB::Stock} } + legs = underlying.map do | the_stock | + if the_stock.is_a? IB::Stock + the_stock + else + IB::Stock.new symbol: the_stock + end.merge( **args ).verify.first + end + + error "only spreads with two underyings of type »IB::Stock« are supported" unless legs.size==2 && are_stocks[legs] + + initialize_spread( legs.first ) do | the_spread | + c_l = legs.zip(ratio).map do |l,r| + action = r >0 ? :buy : :sell + the_spread.add_leg l, action: action, ratio: r.abs + end + the_spread.description = the_description( the_spread ) + the_spread.symbol = legs.map( &:symbol ).sort.join(",") # alphabetical order + the_spread.combo_params = {'NonGuaranteed' => true} + end + end + + def the_description spread + info= spread.legs.map( &:symbol ).zip(spread.combo_legs.map( &:weight )) + "" + + end + + # always route a order as NonGuaranteed + def order_requirements + end + + end # class + end # module +end # module ib diff --git a/plugins/ib/spread-prototypes/straddle.rb b/plugins/ib/spread-prototypes/straddle.rb new file mode 100644 index 0000000..a3527e6 --- /dev/null +++ b/plugins/ib/spread-prototypes/straddle.rb @@ -0,0 +1,70 @@ +module IB + module Straddle + extend SpreadPrototype + class << self + + +# Fabricate a Straddle from a Master-Option +# ----------------------------------------- +# If one Leg is known, the other is simply build by flipping the right +# +# Call with +# IB::Spread::Straddle.fabricate an_option + def fabricate master + + flip_right = ->(the_right){ the_right == :put ? :call : :put } + error "Argument must be a IB::Option" unless [ :option, :futures_option ].include?( master.sec_type ) + + initialize_spread( master ) do | the_spread | + the_spread.add_leg master.essential.verify.first + the_spread.add_leg( master.essential.merge( right: flip_right[master.right], local_symbol: "").verify.first ) + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) + end + end + +# Build Straddle out of an Underlying +# ----------------------------------------- +# Needed attributes: :strike, :expiry +# +# Optional: :trading_class, :multiplier +# +# Call with +# IB::Spread::Straddle.build from: IB::Contract, strike: a_value, expiry: yyyymmm(dd) + def build from:, ** fields + if from.is_a? IB::Option + fabricate from.merge **fields + else + initialize_spread( from ) do | the_spread | + leg_prototype = IB::Option.new from.attributes + .slice( :currency, :symbol, :exchange, :expiry) # use only these fields + .merge(defaults) # add defaults + .merge( fields ) # override attributes with parameters +# puts leg_prototype.attributes + + leg_prototype.sec_type = 'FOP' if from.is_a?( IB::Future ) + the_spread.add_leg leg_prototype.merge( right: :put ).verify.first + the_spread.add_leg leg_prototype.merge( right: :call ).verify.first + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) + end + end + end + + def defaults + super.merge expiry: IB::Option.next_expiry + end + + def requirements + super.merge strike: "the strike of both options", + expiry: "Expiry expressed as »yyyymm(dd)« (String or Integer)" + end + + def the_description spread + my_strike = spread.legs.first.strike + "" + end + + end # class + end # module combo +end # module ib diff --git a/plugins/ib/spread-prototypes/strangle.rb b/plugins/ib/spread-prototypes/strangle.rb new file mode 100644 index 0000000..93d0d4c --- /dev/null +++ b/plugins/ib/spread-prototypes/strangle.rb @@ -0,0 +1,93 @@ +module IB + + module Strangle + + extend SpreadPrototype + class << self + + +# Fabricate a Strangle from a Master-Option +# ----------------------------------------- +# If one Leg is known, the other is build by flipping the right and adjusting the strike by distance +# +# Call with +# IB::Strangle.fabricate an_option, numeric_value + def fabricate master, distance + + flip_right = ->(the_right){ the_right == :put ? :call : :put } + + error "Argument must be an option" unless [:option, :futures_option].include? master.sec_type + + + initialize_spread( master ) do | the_spread | + the_spread.add_leg master.verify.first + the_spread.add_leg master + .essential + .merge( right: flip_right[master.right], + strike: master.strike.to_f + distance.to_f , + local_symbol: '', + con_id: 0 ) + .verify.first + error "Initialisation of Legs failed" if the_spread.legs.size != 2 +# the_spread.description = the_description( the_spread ) + end + end + + +# Build Strangle out of an Underlying +# ----------------------------------------- +# Needed attributes: :strike, :expiry +# +# Optional: :trading_class, :multiplier +# +# Call with +# IB::Strangle.build from: IB::Contract, p: a_value, c: a_value, expiry: yyyymm(dd) + def build from:, p: nil, c: nil, expiry: nil, **fields + underlying = if from.is_a? IB::Option + p ||= from.strike + c ||= from.strike + expiry ||= from.expiry + + details = from.verify.first.contract_detail + IB::Contract.new( con_id: details.under_con_id, + currency: from.currency, + exchange: from.exchange) .verify.first .essential + else + from + end + kind = { :p => p, :c => c } + initialize_spread( underlying ) do | the_spread | + leg_prototype = IB::Option.new from.attributes + .slice( :currency, :symbol, :exchange) + .merge(defaults) + .merge( expiry: expiry ) + .merge( fields ) + + leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future) + the_spread.add_leg leg_prototype.merge( right: :put, strike: kind[:p] ).verify.first + the_spread.add_leg leg_prototype.merge( right: :call, strike: kind[:c] ).verify.first + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) + end + end + + def defaults + super.merge expiry: IB::Future.next_expiry + end + + + def requirements + super.merge p: "the strike of the put option", + c: "the strike of the call option", + expiry: "Expiry expressed as »yyyymm(dd)« (String or Integer) )" + end + + + + def the_description spread + "" + end + + end # class + end # module combo +end # module ib diff --git a/plugins/ib/spread-prototypes/vertical.rb b/plugins/ib/spread-prototypes/vertical.rb new file mode 100644 index 0000000..0adc265 --- /dev/null +++ b/plugins/ib/spread-prototypes/vertical.rb @@ -0,0 +1,83 @@ +module IB + + module Vertical + + extend SpreadPrototype + class << self + + +# Fabricate a Vertical from a Master-Option +# ----------------------------------------- +# If one Leg is known, the other is build by flipping the right and adjusting the strike by distance +# +# Call with +# IB::Vertical.fabricate an_option, buy: {another_strike}, (or) , :sell{another_strike} + def fabricate master, buy: 0, sell: 0 + + error "Argument must be an option" unless [:option, :futures_option].include? master.sec_type + error "Unable to fabricate Vertical. Either :buy or :sell must be specified " if buy.zero? && sell.zero? + + buy = master.strike if buy.zero? + sell = master.strike if sell.zero? + initialize_spread( master ) do | the_spread | + the_spread.add_leg master.merge(strike: sell).verify.first, action: :sell + the_spread.add_leg master.merge(strike: buy).verify.first, action: :buy + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) + end + end + + +# Build Vertical out of an Underlying +# ----------------------------------------- +# Needed attributes: :strikes (buy: strike1, sell: strike2), :expiry, right +# +# Optional: :trading_class, :multiplier +# +# Call with +# IB::Straddle.build from: IB::Contract, buy: a_strike, sell: a_stike, right: {put or call}, expiry: yyyymmm(dd) + def build from:, **fields + underlying = if from.is_a? IB::Option + fields[:right] = from.right unless fields.key?(:right) + fields[:sell] = from.strike unless fields.key(:sell) + fields[:buy] = from.strike unless fields.key?(:buy) + fields[:expiry] = from.expiry unless fields.key?(:expiry) + fields[:trading_class] = from.trading_class unless fields.key?(:trading_class) || from.trading_class.empty? + fields[:multiplier] = from.multiplier unless fields.key?(:multiplier) || from.multiplier.to_i.zero? + details = from.verify.first.contract_detail + IB::Contract.new( con_id: details.under_con_id, + currency: from.currency, + exchange: from.exchange) + .verify.first + .essential + else + from + end + kind = { :buy => fields.delete(:buy), :sell => fields.delete(:sell) } + error "Specification of :buy and :sell necessary, got: #{kind.inspect}" if kind.values.any?(nil) + initialize_spread( underlying ) do | the_spread | + leg_prototype = Option.new underlying.attributes + .slice( :currency, :symbol, :exchange) + .merge(defaults) + .merge( fields ) + leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future) + the_spread.add_leg leg_prototype.merge(strike: kind[:sell]).verify.first, action: :sell + the_spread.add_leg leg_prototype.merge(strike: kind[:buy] ).verify.first, action: :buy + error "Initialisation of Legs failed" if the_spread.legs.size != 2 + the_spread.description = the_description( the_spread ) + end + end + + def defaults + super.merge expiry: IB::Future.next_expiry, + right: :put + end + + + def the_description spread + x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:strike )].transpose + "" + end + end # class + end # module vertical +end # module ib diff --git a/plugins/ib/symbols.rb b/plugins/ib/symbols.rb new file mode 100644 index 0000000..904a2ad --- /dev/null +++ b/plugins/ib/symbols.rb @@ -0,0 +1,118 @@ +=begin + +Plugin that provides helper methods for predefined Contracts + + +Public API +========== + +Extends IB::Contract + +=end + +# These modules are used to facilitate referencing of most popular IB Contracts. +# Like pages in the TWS-GUI, they can be utilised to organise trading and research. +# +# Symbol Allocations are organized as modules. They represent the contents of yaml files in +# +# /lib/symbols/ +# +# Any collection is represented as simple Hash, with __key__ as qualifier and an __IB::Contract__ as value. +# The Value is either a fully prequalified Contract (Stock, Option, Future, Forex, CFD, BAG) or +# a lazy qualified Contract acting as base für further calucaltions and requests. +# +# IB::Symbols.allocate_collection :Name +# +# creates the Module and file. If a previously created file is found, its contents are read and +# the vcollection ist reestablished. +# +# IB::Symbols::Name.add_contract :wfc, IB::Stock.new( symbol: 'WFC' ) +# +# adds the contract and stores it in the yaml file +# +# IB::Symbols::Name.wfc # or IB::Symbols::Name[:wfc] +# +# retrieves the contract +# +# IB::Symbols::Name.all +# +# returns an Array of stored contracts +# +# IB::Symbols::Name.remove_contract :wfc +# +# deletes the contract from the list (and the file) +# +# To finish the cycle +# +# IB::Symbols::Name.purge_collection +# +# deletes the file and erases the collection in memory. +# +# Additional methods can be introduced +# * for individual contracts on the module-level or +# * to organize the list as methods of Array in Module IB::SymbolExtention +# +# +# Contracts can be hardcoded in the required standard-collections as well. +# Note that the :description field is local to ib-ruby, and is NOT part of the standard TWS API. +# It is never transmitted to IB. It's purely used clientside, and you can store any arbitrary +# string that you may find useful there. + +module IB + module Symbols + class Error < StandardError; end + + + + def hardcoded? + !self.methods.include? :yml_file + end + def method_missing(method, *key) + if key.empty? + if contracts.has_key?(method) + contracts[method] + elsif methods.include?(:each) && each.methods.include?(method) + self.each.send method + else + error "contract #{method} not defined. Try »all« for a list of defined Contracts.", :symbol + end + else + error "method missing" + end + end + + def all + contracts.keys.sort rescue contracts.keys + end + def print_all + puts contracts.sort.map{|x,y| [x,y.description].join(" -> ")}.join "\n" + end + def contracts + if @contracts.present? + @contracts + else + @contracts = Hash.new + end + end + def [] symbol + if c=contracts[symbol] + return c + else + # symbol probably has not been predefined, tell user about it + file = self.to_s.split(/::/).last.downcase + msg = "Unknown symbol :#{symbol}, please pre-define it in lib/ib/symbols/#{file}.rb" + error msg, :symbol + end + end + end + + + Connection.current.activate_plugin "verify" + Connection.current.activate_plugin "roll" + Connection.current.activate_plugin "spread-prototypes" + [ :forex, :futures, :stocks, :index, :cfd, :commodity, :options, :combo, :bonds, :abstract ].each do |pt| + Connection.current.activate_plugin "symbols/#{pt.to_s}" + end + +end + diff --git a/plugins/ib/symbols/abstract.rb b/plugins/ib/symbols/abstract.rb new file mode 100644 index 0000000..5f1caa9 --- /dev/null +++ b/plugins/ib/symbols/abstract.rb @@ -0,0 +1,136 @@ +module IB + + # reopen the contract-class and add yml_file + class Contract + + # Reading Contract-Defaults + # + # by default, the yml-file in the base-directory (ib-ruby) is used. + # This method can be overloaded to include a file from a different location + # + # IB::Symbols::Stocks.wfc.yml_file + # => "/home/ubuntu/workspace/ib-ruby/contract_config.yml" + # + def yml_file + File.expand_path('../../../../contract_config.yml',__FILE__ ) + end + end + + module Symbols + +=begin +Creates a Class and associates it with a filename + +raises an IB::Error in case of a conflict with existing class-names +=end + +# set the Pathname to "ib-api/symbols" by default + @@dir= Pathname.new File.expand_path("../../../../symbols/", __FILE__ ) + def self.set_origin directory + p = Pathname.new directory + @@dir = p if p.directory? + rescue Errno::ENOENT + error "Setting up origin for symbol-files --> Directory (#{directory}) does not exist" + end + + def self.allocate_collection name # name might be a string or a symbol + symbol_table = Module.new do + extend Symbols + extend Enumerable + def self.yml_file + @@dir + name.to_s.downcase.split("::").last.concat( ".yml" ) + end + + def self.each &b + contracts.values.each &b + end + end # module new + name = name.to_s.camelize.to_sym + the_collection = if Symbols.send :const_defined?, name + Symbols.send :const_get, name + else + Symbols.const_set name, symbol_table + end + if the_collection.is_a? Symbols + the_collection.send :read_collection if the_collection.all.empty? + the_collection # return_value + else + error "#{the_collection} is already a Class" + nil + end + end + + def purge_collection + yml_file.delete + @contracts = nil + end + +=begin +cuts the Collection in `bunch_count` pieces. Each bunch is delivered to the block. + +Sleeps for `sleeping time` between processing bunches + +Returns count of created bunches +=end + def bunch( bunch_count = 50 , sleeping_time = 1) + en = self.each + the_size = en.size + i = 0 + loop do + the_start = i * bunch_count + the_end = the_start + bunch_count + the_end = the_size -1 if the_end >= the_size + it = the_start .. the_end + yield it.map{|x| en.next rescue nil}.compact + break if the_end == the_size -1 + i+=1 + sleep sleeping_time + end + i -1 # return counts of bunches + end + + def read_collection + if yml_file.exist? + contracts.merge! YAML.unsafe_load_file yml_file rescue contracts + else + yml_file.open( "w"){} + end + end + + def store_collection + yml_file.open( 'w' ){|f| f.write @contracts.to_yaml} + end + + def add_contract symbol, contract + if symbol.is_a? String + symbol.to_sym + elsif symbol.is_a? Symbol + symbol + else + symbol.to_i + end + # ensure that evey Sybmol::xxx.yyy entry has a description + contract.description = contract.to_human[1..-2] if contract.description.nil? + # overwrite contract if existing + contracts[ symbol ] = contract.essential + store_collection + end + + def remove_contract symbol + @contracts.delete symbol + store_collection + end + + + def to_human + self.to_s.split("::").last + end + + + + module Unspecified + extend Symbols + end + + end # module Symbols +end # module IB diff --git a/plugins/ib/symbols/bonds.rb b/plugins/ib/symbols/bonds.rb new file mode 100644 index 0000000..c33597d --- /dev/null +++ b/plugins/ib/symbols/bonds.rb @@ -0,0 +1,28 @@ +# Sample bond contract definitions +module IB + module Symbols + module Bonds + extend Symbols + + def self.contracts + @contracts ||= { + :abbey => IB::Contract.new(:symbol => "ABBEY", + :currency => "USD", + :sec_type => :bond, + :description => "Any ABBEY bond"), + + :ms => IB::Contract.new(:symbol => "MS", + :currency => "USD", + :sec_type => :bond, + :description => "Any Morgan Stanley bond"), + + :wag => IB::Contract.new(:symbol => "WAG", + :currency => "USD", + :sec_type => :bond, + :description => "Any Wallgreens bond"), + } + end + + end + end +end diff --git a/plugins/ib/symbols/cfd.rb b/plugins/ib/symbols/cfd.rb new file mode 100644 index 0000000..067c70e --- /dev/null +++ b/plugins/ib/symbols/cfd.rb @@ -0,0 +1,19 @@ +# Frequently used stock contracts definitions +# TODO: auto-request :ContractDetails from IB if unknown symbol is requested? +module IB + module Symbols + module CFD + extend Symbols + + def self.contracts + @contracts.presence || super.merge( + :dax => IB::Contract.new(:symbol => "IBDE30", sec_type: :cfd, + :currency => "EUR", + :description => "DAX CFD."), + + ) + end + + end + end +end diff --git a/plugins/ib/symbols/combo.rb b/plugins/ib/symbols/combo.rb new file mode 100644 index 0000000..eea3b57 --- /dev/null +++ b/plugins/ib/symbols/combo.rb @@ -0,0 +1,46 @@ +# Frequently used stock contracts definitions +# TODO: auto-request :ContractDetails from IB if unknown symbol is requested? +module IB + module Symbols + module Combo + extend Symbols + + def self.contracts + base = 4800 + exp = IB::Option.next_expiry + @contracts ||= { #super.merge( + stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: base, + expiry: exp, trading_class: 'OESX' ) , + stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: base, back: '2m' , + front: exp, trading_class: 'OESX' ), + stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: base - 200, + expiry: exp), + front: base - 400, back: base), + stoxx_vertical: IB::Vertical.build( from: IB::Symbols::Index.stoxx, + sell: base - 200, buy: base + 200, + right: :put, + expiry: exp, + trading_class: 'OESX'), + zn_calendar: IB::Calendar.fabricate( IB::Symbols::Futures.zn.next_expiry, '3m') , + + dbk_straddle: Bag.new( symbol: 'DBK', currency: 'EUR', exchange: 'EUREX', combo_legs: + [ ComboLeg.new( con_id: 270581032 , action: :buy, exchange: 'DTB', ratio: 1), #DBK Dez20 2018 C + ComboLeg.new( con_id: 270580382, action: :buy, exchange: 'DTB', ratio: 1 ) ], #DBK Dez 20 2018 P + description: 'Option Straddle: Deutsche Bank(20)[Dez 2018]'), + ib_mcd: Bag.new( symbol: 'IBKR,MCD', currency: 'USD', exchange: 'SMART', + combo_legs: [ ComboLeg.new( con_id: 43645865, action: :buy, ratio: 1), # IKBR STK + ComboLeg.new( con_id: 9408, action: :sell,ratio: 1 ) ], # MCD STK + description: 'Stock Spread: Buy Interactive Brokers, sell Mc Donalds'), + + vix_calendar: Bag.new( symbol: 'VIX', currency: 'USD', exchange: 'CFE', + combo_legs: [ ComboLeg.new( con_id: 256038899, action: :buy, exchange: 'CFE', ratio: 1), # VIX FUT 201708 + ComboLeg.new( con_id: 260564703, action: :sell, exchange: 'CFE', ratio: 1 ) ], # VIX FUT 201709 + description: 'VixFuture Calendar-Spread August - September 2017' + ) + } + # ) + end + + end + end +end diff --git a/plugins/ib/symbols/commodity.rb b/plugins/ib/symbols/commodity.rb new file mode 100644 index 0000000..eedc980 --- /dev/null +++ b/plugins/ib/symbols/commodity.rb @@ -0,0 +1,17 @@ +# Frequently used stock contracts definitions +# TODO: auto-request :ContractDetails from IB if unknown symbol is requested? +module IB + module Symbols + module Commodity + extend Symbols + + def self.contracts + @contracts.presence || super.merge( + :xau => IB::Contract.new( symbol: 'XAUUSD', sec_type: :commodity, currency: 'USD', + :description => "London Gold ") + ) + end + + end + end +end diff --git a/plugins/ib/symbols/forex.rb b/plugins/ib/symbols/forex.rb new file mode 100644 index 0000000..f3bc68c --- /dev/null +++ b/plugins/ib/symbols/forex.rb @@ -0,0 +1,41 @@ +module IB + module Symbols + module Forex + extend Symbols + + def self.contracts + @contracts ||= define_contracts + end + + private + + # IDEALPRO is for orders over 20,000 and routes to the interbank quote stream. + # IDEAL is for smaller orders, and has wider spreads/slower execution... generally + # used for smaller currency conversions. IB::Symbols::Forex contracts are pre-defined + # on IDEALPRO, if you need something else please define forex contracts manually. + def self.define_contracts + @contracts = {} + + # use combinations of these currencies for pre-defined forex contracts + currencies = [ "aud", "cad", "chf", "eur", "gbp", "hkd", "jpy", "nzd", "usd" ] + + # create pairs list from currency list + pairs = currencies.product(currencies). + map { |pair| pair.join.upcase unless pair.first == pair.last }.compact + + # now define each contract + pairs.each do |pair| + @contracts[pair.downcase.to_sym] = IB::Forex.new( + :symbol => pair[0..2], + :exchange => "IDEALPRO", + :currency => pair[3..5], + :local_symbol => pair[0..2]+'.'+pair[3..5], + :description => pair + ) + end + + @contracts + end + end + end +end diff --git a/plugins/ib/symbols/futures.rb b/plugins/ib/symbols/futures.rb new file mode 100644 index 0000000..810d908 --- /dev/null +++ b/plugins/ib/symbols/futures.rb @@ -0,0 +1,127 @@ +# The Futures module tries to guess the front month future using a crude algorithm that +# does not take into account expiry/rollover day. This will be valid most of the time, +# but near/after expiry day the next quarter's contract takes over as the volume leader. + +module IB + module Symbols + module Futures + extend Symbols + + + def self.contracts + @contracts.presence ||( super.merge :ym => IB::Future.new(:symbol => "YM", + :expiry => IB::Future.next_expiry, + :exchange => "CBOT", + :currency => "USD", + :description => "Mini-DJIA future"), + :nq => IB::Future.new(:symbol => "NQ", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 20, + :description => "Mini Nasdaq 100 future"), + :micro_nq => IB::Future.new(:symbol => "MNQ", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 2, + :description => "Micro Nasdaq 100 future"), + :es => IB::Future.new(:symbol => "ES", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 50, + :description => "Mini S&P 500 future"), + :micro_es => IB::Future.new(:symbol => "MES", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 5, + :description => "Micro S&P 500 future"), + :russell => IB::Future.new(:symbol => "RTY", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 5, + :description => "Mini Russell 2000 future"), + :micro_russell => IB::Future.new(:symbol => "M2K", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 5, + :description => "Micro Russell 2000 future"), + :zn => IB::Future.new( symbol: 'ZN', + expiry: IB::Future.next_expiry, + currency: 'USD', + multiplier: 1000, + exchange: 'CBOT', + description: 'US Treasury Note -- 10 Years'), + :zb => IB::Future.new( symbol: 'ZB', + expiry: IB::Future.next_expiry, + currency: 'USD', + multiplier: 1000, + exchange: 'CBOT', + description: 'US Treasury Note -- 30 Years'), + :micro_dax => IB::Future.new( symbol: 'DAX', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 1, + description: 'Mini DAX-Future'), + :mini_dax => IB::Future.new( symbol: 'DAX', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 5, + description: 'Mini DAX-Future'), + :dax => IB::Future.new( symbol: 'DAX', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 25, + description: 'DAX-Future'), + :stoxx=> IB::Future.new( symbol: 'ESTX50', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 10, + description: 'EuroStoxx 50 -Future'), + :mini_stoxx=> IB::Future.new( symbol: 'ESTX50', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 1, + description: 'Mini/Micro EuroStoxx 50 -Future'), + :micro_stoxx=> IB::Future.new( symbol: 'ESTX50', exchange: 'EUREX', + expiry: IB::Future.next_expiry, + currency: 'EUR', + multiplier: 1, + description: 'Miico EuroStoxx 50 -Future'), + :gbp => IB::Future.new(:symbol => "GBP", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 62500, + :description => "British Pounds future"), + :eur => IB::Future.new(:symbol => "EUR", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 12500, + :description => "Euro FX future"), + :jpy => IB::Future.new(:symbol => "JPY", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 12500000, + :description => "Japanese Yen future"), + :hsi => IB::Future.new(:symbol => "HSI", + :expiry => IB::Future.next_expiry, + :exchange => "HKFE", + :currency => "HKD", + :multiplier => 50, + :description => "Hang Seng Index future"), + :vix => IB::Future.new(:symbol => "VIX", + :expiry => IB::Future.next_expiry, + :exchange => "CFE", + :currency => "USD", + :description => "CBOE Volatility Index future")) + end + end + end +end diff --git a/plugins/ib/symbols/index.rb b/plugins/ib/symbols/index.rb new file mode 100644 index 0000000..5362407 --- /dev/null +++ b/plugins/ib/symbols/index.rb @@ -0,0 +1,43 @@ +# Frequently used stock contracts definitions +module IB + module Symbols + module Index + extend Symbols + + def self.contracts + @contracts.presence || super.merge( + :dax => IB::Index.new( :symbol => "DAX", :currency => "EUR", exchange: 'EUREX', + :description => "DAX Performance Index."), + :asx => IB::Index.new( :symbol => 'AP', :currency => 'AUD', exchange: 'ASX', + :description => "ASX 200 Index (Base for IndexOptions)" ), + :hsi => IB::Index.new( :symbol => 'HSI', :currency => 'HKD', exchange: 'HKFE', + :description => "Hang Seng Index" ), + :minihsi => IB::Index.new( :symbol => 'MHI', :currency => 'HKD', exchange: 'HKFE', + :description => "Mini Hang Seng Index" ), + :stoxx => IB::Index.new( :symbol => "ESTX50", :currency => "EUR", exchange: 'EUREX', + :description => "Dow Jones Euro STOXX50"), + :spx => IB::Index.new( :symbol => "SPX", :currency => "USD", exchange: 'CBOE', + :description => "S&P 500 Stock Index"), + :vhsi => IB::Index.new( symbol: 'VHSI', exchange: 'HKFE', + :description => "Hang Seng Volatility Index"), + :vasx => IB::Index.new( symbol: 'XVI', exchange: 'ASX', + :description => "ASX 200 Volatility Index") , + :vstoxx => IB::Index.new( :symbol => "V2TX", :currency => "EUR", exchange: 'EUREX', + :description => "VSTOXX Volatility Index"), + :vdax => IB::Index.new( :symbol => "VDAX", exchange: 'EUREX', + :description => "German VDAX Volatility Index"), + :vix => IB::Index.new( :symbol => "VIX", exchange: 'CBOE', + :description => "CBOE Volatility Index"), + :volume => IB::Index.new( symbol: 'VOL-NYSE', exchange: 'NYSE', + description: "NYSE Volume Index" ), + :trin => IB::Index.new( symbol: 'TRIN-NYSE', exchange: 'NYSE', + description: "NYSE TRIN (or arms) Index"), + :tick => IB::Index.new( symbol: 'TICK-NYSE', exchange: 'NYSE', + description: "NYSE TICK Index"), + :a_d => IB::Index.new( symbol: 'AD-NYSE', exchange: 'NYSE', + description: "NYSE Advance Decline Index") ) + end + + end + end +end diff --git a/plugins/ib/symbols/options.rb b/plugins/ib/symbols/options.rb new file mode 100644 index 0000000..4158ab0 --- /dev/null +++ b/plugins/ib/symbols/options.rb @@ -0,0 +1,99 @@ +# Option contracts definitions. +# TODO: add next_expiry and other convenience from Futures module. +# Notice: OSI-Notation is broken +module IB + module Symbols + module Options + extend Symbols + + ## usage: IB::Symbols::Options.stoxx.merge( strike: 5000, expiry: 202404 ) + ## IB::Symbols::Options.stoxx.merge( strike: 5000 ).next_expiry => fetch the next regulary + ## monthly option (3.rd friday) + def self.contracts + @contracts ||= { + stoxx: IB::Option.new(symbol: :ESTX50, + expiry: IB::Option.next_expiry , + right: :put, + trading_class: 'OESX', + currency: 'EUR', + exchange: 'EUREX', + description: "Monthly settled ESTX50 Options"), + spx: IB::Option.new( symbol: :SPX, + expiry: IB::Option.next_expiry , + right: :put, + trading_class: 'SPX', + currency: 'USD', + exchange: 'SMART', + description: "Monthly settled SPX options"), + spxw: IB::Option.new( symbol: :SPX, + expiry: IB::Option.next_expiry , + right: :put, + trading_class: 'SPXW', + currency: 'USD', exchange: 'SMART', + description: "Daily settled SPX options"), + xsp: IB::Option.new( symbol: 'XSP', + expiry: IB::Option.next_expiry , + right: :put, + trading_class: 'XSP', + currency: 'USD', + exchange: 'SMART', + description: "Daily settled Mini-SPX options"), + :spy => IB::Option.new( :symbol => :SPY, + :expiry => IB::Option.next_expiry, + :right => :put, + :currency => "USD", + :exchange => 'SMART', + :description => "SPY Put next expiration"), + :rut => IB::Option.new( :symbol => :RUT, + :expiry => IB::Option.next_expiry, + :right => :put, + :currency => "USD", + :exchange => 'SMART', + :trading_class => 'RUT', + description: "Monthly settled RUT options"), + :rutw => IB::Option.new( :symbol => :RUT, + :expiry => IB::Option.next_expiry, + :right => :put, + :currency => "USD", + :exchange => 'SMART', + description: "Weekly settled RUT options"), + :russell => IB::Option.new( :symbol => :RUT, # :russell == :rut ! + :expiry => IB::Option.next_expiry, + :right => :put, + :currency => "USD", + :exchange => 'SMART', + description: "Monthly settled RUT options"), + :mini_russell => IB::Option.new( :symbol => :MRUT, + :expiry => IB::Option.next_expiry, + :right => :put, + :currency => "USD", + :exchange => 'SMART', + :description => "Weekly settled Mini-Russell2000 options"), + :aapl => IB::Option.new( :symbol => "AAPL", + :expiry => IB::Option.next_expiry, + :right => "C", + :strike => 150, + :exchange => 'SMART', + :currency => 'USD', + :description => "Apple Call 130"), + + :ibm => IB::Option.new( symbol: 'IBM', + exchange: 'SMART', + right: :put, + expiry: IB::Option.next_expiry , + description: 'IBM-Option'), + :ibm_lazy_expiry => IB::Option.new( symbol: 'IBM', + right: :put, + strike: 180, + exchange: 'SMART', + description: 'IBM-Option Chain with strike 180'), + :ibm_lazy_strike => IB::Option.new( symbol: 'IBM', + right: :put, + exchange: 'SMART', + expiry: IB::Option.next_expiry, + description: 'IBM-Option Chain ( monthly expiry)') + } + end + end + end +end diff --git a/plugins/ib/symbols/stocks.rb b/plugins/ib/symbols/stocks.rb new file mode 100644 index 0000000..b414a3d --- /dev/null +++ b/plugins/ib/symbols/stocks.rb @@ -0,0 +1,44 @@ +# Frequently used stock contracts definitions +# TODO: auto-request :ContractDetails from IB if unknown symbol is requested? +module IB + module Symbols + module Stocks + extend Symbols + + def self.contracts + @contracts.presence || super.merge( + :ib_smart => IB::Stock.new( :symbol => 'IBKR', + :description => 'Interactive Brokers Stock with smart exchange setting'), + :ib => IB::Stock.new( :symbol=> 'IBKR', exchange: 'ISLAND', + :description => 'Interactive Brokers Stock'), + :aapl => IB::Stock.new( :symbol => "AAPL", + :currency => "USD", + :description => "Apple Inc."), + + :msft_conid => IB::Stock.new( con_id: 272093, + currency: :usd , + description: 'Microsoft selected by its con-id'), + :msft => IB::Stock.new( symbol: 'MSFT', + description: 'Microsoft selected by its symbol'), + :msft_island =>IB::Stock.new( symbol: 'MSFT', primary_exchange: 'ISLAND', + description: 'Microsoft, primary trading @ ISLAND'), + :vxx => IB::Stock.new( :symbol => "VXX", + :exchange => "ARCA", + :description => "iPath S&P500 VIX short term Futures ETN"), + :wfc => IB::Stock.new( :symbol => "WFC", + :exchange => "NYSE", + :currency => "USD", + :description => "Wells Fargo"), + :sie => IB::Stock.new( symbol: 'SIE', + currency: 'EUR', + description: 'Siemens AG'), + :wrong => IB::Stock.new( :symbol => "QEEUUE", + :exchange => "NYSE", + :currency => "USD", + :description => "Non-existent stock") + ) + end + + end + end +end diff --git a/plugins/ib/verify.rb b/plugins/ib/verify.rb new file mode 100644 index 0000000..bda7f9d --- /dev/null +++ b/plugins/ib/verify.rb @@ -0,0 +1,226 @@ +module IB +=begin rdoc + +Plugin that provides Verifying a contract + +Public API +========== + +Extends IB::Contract + +* verify + + returns an array of suitable IB::Contracts +```ruby + a = Stock.new symbol: 'AA' + aa = a.verify.first +``` + + an optional block may be used to modify and filter the tws-response + +```ruby + f = IB::Future.new symbol: 'M2K' + con_ids = f.verify{ |c| c.con_id } + => [412889018, 428519982, 446091466, 461318872, 477836981] +``` +=end + + module Verify + + + # IB::Contract#Verify + + # verifies the contract + # + # returns the number of contracts returned by the TWS. + # + # + # The method accepts a block. The queried contract-Object is accessible there. + # If multiple contracts are returned by the tws, the block is executed with each of these contracts. + # + # + # Verify returns an _Array_ of contracts. The operation leaves the contract untouched. + # + # + # Returns nil if the contract could not be verified. + # + # > s = Stock.new symbol: 'AA' + # => #"AA", :con_id=>0, :right=>"", :include_expired=>false, + # :sec_type=>"STK", :currency=>"USD", :exchange=>"SMART"} + # > sp = s.verify.first.essential + # => #"AA", :con_id=>251962528, :exchange=>"SMART", :currency=>"USD", + # :strike=>0.0, :local_symbol=>"AA", :multiplier=>0, :primary_exchange=>"NYSE", + # :trading_class=>"AA", :sec_type=>"STK", :right=>"", :include_expired=>false} + # + # > s = Stock.new symbol: 'invalid' + # => @attributes={:symbol=>"invalid", :sec_type=>"STK", :currency=>"USD", :exchange=>"SMART"} + # > sp = s.verify + # => [] + # + # Takes a Block to modify the queried contracts + # + # f = Future.new symbol: 'M2K' + # con_ids = f.verify{ |c| c.con_id } + # [412889018, 428519982, 446091466, 461318872, 477836981] + # + # + # Parameter: thread: (true/false) + # + # If multiple contracts are to be verified, they can be queried simultaneously. + # IB::Symbols::W500.map{|c| c.verify(thread: true){ |vc| do_something }}.join + + def verify thread: nil, &b + if thread + Thread.new { _verify &b } + else + i = 0 + begin + _verify &b + rescue IB::VerifyError + i += 1 + if i < 3 + sleep 1 + retry + else + raise + end + end + end + end # def + + # returns a hash + def necessary_attributes + + v= { stock: { currency: 'USD', exchange: 'SMART', symbol: nil}, + option: { currency: 'USD', exchange: 'SMART', right: 'P', expiry: nil, strike: nil, symbol: nil}, + future: { currency: 'USD', exchange: nil, expiry: nil, symbol: nil }, + forex: { currency: 'USD', exchange: 'IDEALPRO', symbol: nil } + } + sec_type.present? ? v[sec_type] : { con_id: nil, exchange: 'SMART' } # enables to use only con_id for verifying + # if the contract allows SMART routing + end + + + private + + # Base method to verify a contract + # + # if :thread is given, the method subscribes to messages, fires the request and returns the thread, that + # receives the exit-condition-message + # + # otherwise the method waits until the response form tws is processed + # + # + # if :update is true, the attributes of the Contract itself are adapted + # + # otherwise the Contract is untouched + def _verify &b # :nodoc: + ib = Connection.current + error "No Connection" unless ib.is_a? Connection + # we generate a Request-Message-ID on the fly + error "Either con_id or sec_type have to be set", :verify if con_id.to_i.zero? && sec_type.blank? + # define local vars which are updated within the query-block + received_contracts = [] + queue = Queue.new + message_id = nil + + # a tws-request is suppressed for bags and if the contract_detail-record is present + tws_request_not_necessary = bag? || contract_detail.is_a?( ContractDetail ) + + if tws_request_not_necessary + yield self if block_given? + return [self] # return an array! + else # subscribe to ib-messages and describe what to do + a = ib.subscribe(:Alert, :ContractData, :ContractDataEnd) do |msg| + case msg + when Messages::Incoming::Alert + ## do not throw an error here, asynchronous operation! + ## just notice failure in log and return nil instead of contract-object + if msg.code == 200 && msg.error_id == message_id + ib.logger.error { "Not a valid Contract :: #{self.to_human} " } + queue.push "InvalidContract" + end + when Messages::Incoming::ContractData + if msg.request_id.to_i == message_id + c = if block_given? + yield msg.contract + else + msg.contract + end + queue.push c unless c.nil? + end + when Messages::Incoming::ContractDataEnd + queue.close if msg.request_id.to_i == message_id + end # case + end # subscribe + + ### send the request ! + # contract_to_be_queried = con_id.present? ? self : query_contract + # if no con_id is present, the given attributes are checked by query_contract + # if contract_to_be_queried.present? # is nil if query_contract fails + message_id = ib.send_message :RequestContractData, :contract => query_contract + + Thread.new do + (0 .. 10).each{ sleep 0.1 } + queue.push "TimeOut" unless queue.closed? + end + + while r = queue.pop + if r.is_a? IB::Contract + received_contracts << r + else + error "No data received from IB-Servers", :verify if r == "TimeOut" + end + end + ib.unsubscribe a + end + received_contracts # return contracts + end + + # Generates an IB::Contract with the required attributes to retrieve a unique contract from the TWS + # + # Background: If the tws is queried with a »complete« IB::Contract, it fails occasionally. + # So – even to update its contents, a defined subset of query-parameters has to be used. + # + # The required data-fields are stored in a yaml-file and fetched by #YmlFile. + # + # If `con_id` is present, only `con_id` and `exchange` are transmitted to the tws. + # Otherwise a IB::Stock, IB::Option, IB::Future or IB::Forex-Object with necessary attributes + # to query the tws is build (and returned) + # + # If Attributes are missing, an IB::VerifyError is fired, + # This can be trapped with + # rescue IB::VerifyError do ... + + def query_contract( invalid_record: true ) # :nodoc: + # don't raise a verify error at this time. Contract.new con_id= xxxx, currency = 'xyz' is also valid + ## raise VerifyError, "Querying Contract failed: Invalid Security Type" unless SECURITY_TYPES.values.include? sec_type + + ## the yml contains symbol-entries + ## these are converted to capitalized strings + items_as_string = ->(i){i.map{|x,y| x.to_s.capitalize}.join(', ')} + ## here we read the corresponding attributes of the specified contract + item_values = ->(i){ i.map{|x,y| self.send(x).presence || y }} + ## and finally we create a attribute-hash to instantiate a new Contract + ## to_h is present only after ruby 2.1.0 + item_attributehash = ->(i){ i.keys.zip(item_values[i]).to_h } + ## now lets proceed, but only if no con_id is present + if con_id.blank? || con_id.zero? + # if item_values[necessary_attributes].any?( &:nil? ) + # raise VerifyError, "#{items_as_string[necessary_attributes]} are needed to retrieve Contract, + # got: #{item_values[necessary_attributes].join(',')}" + # end + # Contract.build item_attributehash[necessary_items].merge(:sec_type=> sec_type) # return this + Contract.build self.invariant_attributes # return this + else # its always possible, to retrieve a Contract if con_id and exchange or are present + Contract.new con_id: con_id , :exchange => exchange.presence || item_attributehash[necessary_attributes][:exchange].presence || 'SMART' # return this + end # if + end # def + end # module verify + +class Contract + include Verify +end +end #module ib diff --git a/spec/account_helper.rb b/spec/account_helper.rb index 66f652a..620cf14 100644 --- a/spec/account_helper.rb +++ b/spec/account_helper.rb @@ -1,25 +1,25 @@ require 'main_helper' ## call with -## it_behaves_like 'Valid Account Object' do -## let( :the_account_object ){ some_object } -## end +## it_behaves_like 'Valid Account Object' do +## let( :the_account_object ){ some_object } +## end shared_examples_for 'Valid Account Object' do - subject{ the_account_object } - it{ is_expected.to be_a IB::Account } - its( :account) { is_expected.to be_a String } - its( :save ){ is_expected.to be_truthy } + subject{ the_account_object } + it{ is_expected.to be_a IB::Account } + its( :account) { is_expected.to be_a String } + its( :save ){ is_expected.to be_truthy } end ## call with -## it_behaves_like 'Valid AccountValue Object' do -## let( :the_account_value_object ){ some_object } -## end +## it_behaves_like 'Valid AccountValue Object' do +## let( :the_account_value_object ){ some_object } +## end shared_examples_for 'Valid AccountValue Object' do - subject{ the_account_value_object } - it { is_expected.to be_a IB::AccountValue } - its( :key ) { is_expected.to be_a Symbol } - its( :value ) { is_expected.to be_a String } - its( :currency ) { is_expected.to be_a String } + subject{ the_account_value_object } + it { is_expected.to be_a IB::AccountValue } + its( :key ) { is_expected.to be_a Symbol } + its( :value ) { is_expected.to be_a String } + its( :currency ) { is_expected.to be_a String } end ### Helpers for placing and verifying orders ### old @@ -41,7 +41,7 @@ it { is_expected.to be_an IB::Messages::Incoming::AccountValue } its(:data) { is_expected.to be_a Hash } its(:account) { is_expected.to match /\w\d/ } - its(:account_value ){ is_expected.to be_a IB::AccountValue } + its(:account_value ){ is_expected.to be_a IB::AccountValue } # its(:key) { is_expected.to be_a String } # its(:value) { is_expected.to be_a String } # its(:currency) { is_expected.to be_a String } @@ -54,11 +54,11 @@ it { is_expected.to be_an IB::Messages::Incoming::PortfolioValue } its( :contract ) { is_expected.to be_a IB::Contract } its( :data ) { is_expected.to be_a Hash } - its( :portfolio_value ){is_expected.to be_a IB::PortfolioValue } + its( :portfolio_value ){is_expected.to be_a IB::PortfolioValue } its( :account ) { is_expected.to match /\w\d/ } its( :to_human ) { is_expected.to match /PortfolioValue/ } - + # its(:position) { should be_a BigDecimal } # puts # its(:market_price) { should be_a BigDecimal } @@ -82,8 +82,8 @@ it { is_expected.to be_an IB::Messages::Incoming::ReceiveFA } its(:message_type) { is_expected.to eq :ReceiveFA } its(:message_id) { is_expected.to eq 16 } - its(:accounts) {is_expected.to be_an Array} - its( :buffer ){ is_expected.to be_empty } + its(:accounts) {is_expected.to be_an Array} + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 16 diff --git a/spec/combo_helper.rb b/spec/combo_helper.rb index 8c7284c..f7628e8 100644 --- a/spec/combo_helper.rb +++ b/spec/combo_helper.rb @@ -2,7 +2,7 @@ # Define butterfly def butterfly symbol, expiry, right, *strikes - ib = IB::Connection.current + ib = IB::Connection.current raise 'Unable to create butterfly, no connection' unless ib && ib.connected? legs = strikes.zip([1, -2, 1]).map do |strike, weight| @@ -32,41 +32,47 @@ def butterfly symbol, expiry, right, *strikes def atm_option stock # returns the ATM-Put-Option of the given stock atm = stock.atm_options - atm[atm.keys.at(1)].first + atm[atm.keys.at(1)].first end RSpec.shared_examples 'a valid Estx Combo' do - its( :exchange ) { should eq 'DTB' } - its( :symbol ) { should eq "ESTX50" } - its( :market_price ) { should be_a Numeric } + its( :exchange ) { should eq 'EUREX' } + its( :symbol ) { should eq "ESTX50" } +# its( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid ES-FUT Combo' do - its( :exchange ) { should eq 'GLOBEX' } - its( :symbol ) { should eq "ES" } - its( :market_price ) { should be_a Numeric } + its( :exchange ) { should eq 'CME' } + its( :symbol ) { should eq "ES" } +# its( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid ZN-FUT Combo' do - its( :exchange ) { should eq 'ECBOT' } - its( :symbol ) { should eq "ZN" } - its( :market_price ) { should be_a Numeric } + its( :exchange ) { should eq 'CBOT' } + its( :symbol ) { should eq "ZN" } +# its( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid wfc-stock Combo' do - its( :exchange ) { should eq 'EDGX' } - its( :symbol ) { should eq "WFC" } - its( :market_price ) { should be_a Numeric } + its( :exchange ) { should eq 'SMART' } + its( :symbol ) { should eq "WFC" } +# its( :market_price ) { should be_a Numeric } +end +RSpec.shared_examples 'a valid apple-stock Combo' do + + its( :exchange ) { should eq 'SMART' } + its( :symbol ) { should eq "AAPL" } +# its( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid Spread' do - its( :sec_type ) { should eq :bag } - its( :legs ){ should be_a Array } + its( :sec_type ) { should eq :bag } + its( :legs ){ should be_a Array } - + end diff --git a/spec/contract_helper.rb b/spec/contract_helper.rb index 28dcccd..66cd239 100644 --- a/spec/contract_helper.rb +++ b/spec/contract_helper.rb @@ -9,50 +9,50 @@ def request_con_id sample: SAMPLE - ib = IB::Connection.current - while ib.nil? - establish_connection unless ib.is_a? IB::Connection - ib = IB::Connection.current - end - - ib.clear_received - raise 'Unable to verify contract, no connection' unless ib && ib.connected? + ib = IB::Connection.current + while ib.nil? + establish_connection unless ib.is_a? IB::Connection + ib = IB::Connection.current + end + + ib.clear_received + raise 'Unable to verify contract, no connection' unless ib && ib.connected? - ib.send_message :RequestContractDetails, contract: sample - ib.wait_for :ContractDetailsEnd + ib.send_message :RequestContractDetails, contract: sample + ib.wait_for :ContractDetailsEnd - ib.received[:ContractData].contract.map &:con_id # return an array of con_id's + ib.received[:ContractData].contract.map &:con_id # return an array of con_id's end RSpec.shared_examples 'a complete Contract Object' do -# subject{ the_contract } - it_behaves_like 'a valid Contract Object' +# subject{ the_contract } + it_behaves_like 'a valid Contract Object' it { is_expected.to be_an IB::Contract } - its( :contract_detail ){ is_expected.to be_a IB::ContractDetail } - its( :primary_exchange){ is_expected.to be_a String } + its( :contract_detail ){ is_expected.to be_a IB::ContractDetail } + its( :primary_exchange){ is_expected.to be_a String } end RSpec.shared_examples 'a valid Contract Object' do -# subject{ the_contract } +# subject{ the_contract } it { is_expected.to be_an IB::Contract } - its( :con_id ){ is_expected.to be_empty.or be_a(Numeric) } - its( :contract_detail ){ is_expected.to be_nil.or be_a(IB::ContractDetail) } + its( :con_id ){ is_expected.to be_empty.or be_a(Numeric) } + its( :contract_detail ){ is_expected.to be_nil.or be_a(IB::ContractDetail) } its( :symbol ){ is_expected.to be_a String } its( :local_symbol ){ is_expected.to be_a String } its( :currency ){ is_expected.to be_a String } - its( :sec_type ){ is_expected.to be_a(Symbol).and satisfy { |sec_type| IB::SECURITY_TYPES.values.include?(sec_type) } } + its( :sec_type ){ is_expected.to be_a(Symbol).and satisfy { |sec_type| IB::SECURITY_TYPES.values.include?(sec_type) } } its( :trading_class ){ is_expected.to be_a String } - its( :exchange ){ is_expected.to be_a String } - its( :primary_exchange){ is_expected.to be_nil.or be_a(String) } + its( :exchange ){ is_expected.to be_a String } + its( :primary_exchange){ is_expected.to be_nil.or be_a(String) } end RSpec.shared_examples 'ContractData Message' do - subject{ the_message } + subject{ the_message } it { is_expected.to be_an IB::Messages::Incoming::ContractData } - its( :contract ){ is_expected.to be_a IB::Contract } - its( :contract_details ){ is_expected.to be_a IB::ContractDetail } + its( :contract ){ is_expected.to be_a IB::Contract } + its( :contract_details ){ is_expected.to be_a IB::ContractDetail } its( :message_id ){ is_expected.to eq 10 } its( :version ){ is_expected.to eq 8 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 10 diff --git a/spec/ib/connect_spec.rb b/spec/ib/connect_spec.rb index c843786..ddf7bea 100644 --- a/spec/ib/connect_spec.rb +++ b/spec/ib/connect_spec.rb @@ -1,12 +1,76 @@ require "main_helper" +require 'rspec/given' describe "Connect to Gateway or TWS" do - before(:all){ establish_connection } + before(:all){ establish_connection } after(:all) { close_connection } - - context "A new connection" do - it{ expect( IB::Connection.current ).to be_a IB::Connection } - end + + context "A new connection" do + Given( :connection ){ IB::Connection.current } + Then { connection.is_a? IB::Connection } + end + + context "Workflow States " do + context "ready" do + Given( :connection ){ IB::Connection.current } + + Then { connection.ready? } + Then { connection.workflow_state == 'ready' } + end + + context "disconnected" do +# Given( :connection ){ IB::Connection.current } + it "initiate disconnect" do + + + ib = IB::Connection.current + expect { ib.disconnect! }.to change { ib.workflow_state }.to 'disconnected' + expect( ib.disconnected? ).to be_truthy + expect( ib.ready? ).to be_falsy + end + end + end + + +# it "the received array is active" do +# expect( IB::Connection.current.received).to be_an Hash +# expect( IB::Connection.current.received.keys).to include :Alert +# end +# +# it "clients are NOT present" do +# expect{ IB::Connection.current.clients }.to raise_error NoMethodError +# end +# it "can be disconnected" do +# end +# + context "load plugins in the fly" do + + it "connection-tools can be loaded in ready state" do + ib = IB::Connection.current + expect { ib.try_connection! }.to change{ ib.workflow_state }.to 'ready' + expect( ib.ready? ).to be_truthy + expect{ ib.check_connection }.to raise_error NoMethodError + + ib.activate_plugin :connection_tools + expect( ib.check_connection ).to be_truthy + expect( ib.ready? ).to be_truthy # unchanged + + + end + + it "state `account-based operations` can be loaded through managed-accounts plugin" do + ib = IB::Connection.current + expect( ib.workflow_state ).to eq 'ready' + ib.activate_plugin :managed_accounts , :connection_tools + expect( ib.workflow_state).to eq "ready" + expect( ib.plugins ).to include "managed-accounts" + expect{ ib.clients }.to raise_error NoMethodError + expect { ib.initialize_managed_accounts! }.to change{ ib.workflow_state }.to 'account_based_operations' + expect( ib.clients ).to be_a Array + expect { ib.disconnect! }.to change{ ib.workflow_state }.to 'disconnected' + end + end +# end diff --git a/spec/ib/connection_spec.rb b/spec/ib/connection_spec.rb new file mode 100644 index 0000000..791b323 --- /dev/null +++ b/spec/ib/connection_spec.rb @@ -0,0 +1,33 @@ +require "spec_helper" + +describe IB::Connection do + Given( :ib ) { IB::Connection.new } + Then { ib.is_a? IB::Connection } + + # Check if all Messages are defined + # There are 51 Incoming Message classes + Given( :in_classes ){ IB::Messages::Incoming::Classes } + Then{ in_classes.is_a? Hash } + Then{ in_classes.size == 51 } + + Given( :out_classes ){ IB::Messages::Outgoing::Classes } + Then{ out_classes.is_a? Hash } + Then{ out_classes.size == 53 } +end + +describe "Connection tests" do + it "connect to localhost" do + c = IB::Connection.new host: OPTS[:connection][:host], port: OPTS[:connection][:port] + expect( c ).to be_a IB::Connection + c.try_connection! + expect( c.connected? ).to be_truthy + + end + it "connect to localhost with host:port syntax" do # expected: no GUI-TWS is running on localhost + c = IB::Connection.new host: '127.0.0.1:4001' + expect( c ).to be_a IB::Connection + expect{ c.try_connection! }.to raise_error Errno::ECONNREFUSED + + end +end + diff --git a/spec/ib/contracts/butterfly_spec.rb b/spec/ib/contracts/butterfly_spec.rb new file mode 100644 index 0000000..c515414 --- /dev/null +++ b/spec/ib/contracts/butterfly_spec.rb @@ -0,0 +1,51 @@ +require 'combo_helper' +require 'order_helper' + +RSpec.describe "IB::Butterfly" do + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'spread-prototypes' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + ib.activate_plugin 'market-price' + + ib.subscribe( :Alert ){|y| puts y.to_human } + end + +after(:all) do + close_connection +end + + let( :the_option ){ IB::Symbols::Options.stoxx.merge( strike: 5000 ) } + let( :the_bag ){ IB::Symbols::Combo::stoxx_butterfly } + +context "initialize with master-option" do + subject { IB::Butterfly.fabricate( the_option, back: the_option.strike - 50, front: the_option.strike + 50 )} + it{ puts subject.as_table } + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + + +end + +context "initialize with underlying" do + subject { IB::Butterfly.build( from: IB::Symbols::Index.stoxx, + strike: 5000, + front: 4950, + back: 5050, + trading_class: 'OESX' ) } + it{ puts subject.as_table } + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + +context "create a limit-order" do + subject { IB::Limit.order contract: IB::Symbols::Combo.stoxx_butterfly, size: 1, price: 25, + account: ACCOUNT, what_if: true } + it{ puts subject.as_table } + it{ puts subject.contract.as_table } + it_behaves_like 'serialize limit order fields' +end +end diff --git a/spec/ib/contracts/calendar_spec.rb b/spec/ib/contracts/calendar_spec.rb new file mode 100644 index 0000000..42caa3d --- /dev/null +++ b/spec/ib/contracts/calendar_spec.rb @@ -0,0 +1,47 @@ +require 'combo_helper' + +RSpec.describe "IB::Calendar" do + before(:all) do + establish_connection :gateway + IB::Connection.current.activate_plugin 'spread-prototypes' + IB::Connection.current.activate_plugin 'order-prototypes' + IB::Connection.current.activate_plugin 'symbols' + IB::Connection.current.activate_plugin 'roll' + IB::Connection.current.activate_plugin 'market-price' + IB::Connection.current.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + let ( :the_option ){ IB::Symbols::Options.stoxx.merge strike: 4800, right: :call, trading_class: 'OESX' } + + context "initialize with master-option and second expiry" do + subject { IB::Calendar.fabricate the_option, IB::Option.next_expiry( Date.today + 30 ) } + it{ puts subject.as_table } + it{ is_expected.to be_a IB::Bag } + it_behaves_like 'a valid Estx Combo' + end + + context "initialize with underlying, strike and distance of the two legs" do + subject{ IB::Calendar.build( from: IB::Symbols::Index.stoxx, + strike: 4900, + right: :put, + trading_class: 'OESX', + front: IB::Option.next_expiry , + back: '3m' + ) } + + it{ puts subject.as_table } + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + context "initialize with Future-contract and distance" do + subject{ IB::Calendar.fabricate IB::Symbols::Futures.zn.next_expiry, '3m' } + + it{ puts subject.as_table } + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ZN-FUT Combo' + end +end diff --git a/spec/ib/contracts/spread_spec.rb b/spec/ib/contracts/spread_spec.rb new file mode 100644 index 0000000..bee0b30 --- /dev/null +++ b/spec/ib/contracts/spread_spec.rb @@ -0,0 +1,93 @@ +require 'order_helper' + +RSpec.shared_examples 'a valid NQ-FUT Combo' do + + its( :exchange ) { should eq 'CME' } + its( :symbol ) { should eq "NQ" } +# its( :market_price ) { should be_a Numeric } +end + +RSpec.shared_examples 'serialize two Combo-legs' do + + it "the con_id's are serialized" do + con_ids = subject.contract.combo_legs.map &:con_id + buy_and_sell = subject.contract.combo_legs.map{|y| y.action.to_s.upcase} + exchanges = subject.contract.combo_legs.map &:exchange + expect( subject.serialize_combo_legs(subject.contract).size ).to eq 5 + expect( subject.serialize_combo_legs(subject.contract).flatten.slice(1,8 )).to eq [ con_ids[0], + 1, # quantity + buy_and_sell[0], + exchanges[0],0,0,"",-1 ] + expect( subject.serialize_combo_legs.flatten.slice(9,8 )).to eq [ con_ids[1], + 1, # quantity + buy_and_sell[1], + exchanges[1],0,0,"",-1 ] + +# expect( subject.serialize_combo_legs[1..2].map{|y| y.at 2} ).to eq con_ids + end +end + +RSpec.describe "IB::Spread" do + let( :the_option ) { IB::Symbols::Options.stoxx.merge( strike: 5000 ) } + let( :the_spread ) { IB::Calendar.fabricate IB::Symbols::Futures.nq, '3m' } + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.subscribe( :Alert ){|y| puts y.to_human } + ib.activate_plugin 'verify' + ib.activate_plugin 'spread-prototypes' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + ib.activate_plugin 'market-price' + end + + after(:all) do + close_connection + end + + + context "initialize by fabrication" do + + subject{ the_spread } + it{ is_expected.to be_a IB::Bag } + it_behaves_like 'a valid NQ-FUT Combo' + + it "has proper combo-legs" do + expect( subject.combo_legs.first.side ).to eq :buy + expect( subject.combo_legs.last.side ).to eq :sell + end + end + + context "serialize the spread in the order process" do + subject { IB::Limit.order contract: the_spread, size: 1, price: 45 } + + it_behaves_like "serialize limit order fields" + it_behaves_like "serialize two Combo-legs" + it { expect( subject.serialize_combo_legs(the_spread) ).to eq [ the_spread.serialize_legs, + 0 ,[], 0 , [] ] } + # leg-prices + combo-params + + + + end + + context "leg management" do + subject { the_spread } + + its( :legs ){ is_expected.to have(2).elements } + + it "add a leg" do + expect{ subject.add_leg( the_option ) }.to change{ subject.legs.size }.by(1) + end + + it "remove a leg" do + # non existing leg + expect{ subject.remove_leg( the_option ) }.not_to change{ subject.legs.size } + +# subject.add_leg( the_option ) + expect{ subject.remove_leg( 0 ) }.to change{ subject.legs.size }.by(-1) + end + end + +end diff --git a/spec/ib/contracts/stock_spread_spec.rb b/spec/ib/contracts/stock_spread_spec.rb new file mode 100644 index 0000000..b32778d --- /dev/null +++ b/spec/ib/contracts/stock_spread_spec.rb @@ -0,0 +1,75 @@ +require 'combo_helper' +RSpec.shared_examples 'spread_params' do + +end + +RSpec.describe "IB::StockSpread" do + before(:all) do + establish_connection :gateway + IB::Connection.current.activate_plugin 'spread-prototypes' + IB::Connection.current.activate_plugin 'order-prototypes' + IB::Connection.current.activate_plugin 'symbols' + IB::Connection.current.activate_plugin 'market-price' + IB::Connection.current.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + + context "initialize without ratio" do + subject { IB::StockSpread.fabricate 'T','GE' } + it{ is_expected.to be_a IB::Spread } +# it_behaves_like 'a valid Estx Combo' + + its(:symbol){ is_expected.to eq "GE,T" } + its( :legs ){ is_expected.to have(2).elements} +# its( :market_price ){ is_expected.to be_a BigDecimal } + + it "can be printed as table" do + puts subject.as_table + end + + end + + context "initialize with ratio" do + subject { IB::StockSpread.fabricate IB::Stock.new( symbol:'T' ), IB::Stock.new(symbol: 'GE'), ratio:[1,-3] } + it{ is_expected.to be_a IB::Spread } +# it_behaves_like 'a valid Estx Combo' + it "can be printed as table" do + puts subject.as_table + end + + its( :symbol){ is_expected.to eq "GE,T" } + its( :legs ){ is_expected.to have(2).elements} + its( :market_price ){ is_expected.to be_a BigDecimal } + + it "the ratio is met " do + ratio = subject.combo_legs.map &:ratio + sides = subject.combo_legs.map &:side + + expect( ratio ).to eq [ 1, 3 ] + expect( sides ).to eq [ :buy, :sell ] + end + + end + context "initialize with (reverse) ratio" do + subject { IB::StockSpread.fabricate IB::Stock.new( symbol:'GE' ), IB::Stock.new(symbol: 'T'), ratio:[1, -3] } + it{ is_expected.to be_a IB::Spread } + + its(:symbol){ is_expected.to eq "GE,T" } + its( :legs ){ is_expected.to have(2).elements} + its( :market_price ){ is_expected.to be_a BigDecimal } + + end + + context "initialize with more then two stocks" do + + it "fabricate raises an error " do + + expect { IB::StockSpread.fabricate 'GE','T', "A", ratio:[1, -3, 5] }.to raise_error IB::Error + end + end + subject { IB::StockSpread.fabricate IB::Stock.new( symbol:'GE' ), IB::Stock.new(symbol: 'T'), ratio:[1, -3] } +end diff --git a/spec/ib/contracts/straddle_spec.rb b/spec/ib/contracts/straddle_spec.rb new file mode 100644 index 0000000..9459015 --- /dev/null +++ b/spec/ib/contracts/straddle_spec.rb @@ -0,0 +1,57 @@ +require 'combo_helper' +STRIKE_ESTX = 4800 # fill in an appropiate strike for EuroStoxx +STRIKE_ES = 5800 # same for ES-Future +STRIKE_WFC = 70 # same for Wells Fargo +RSpec.describe "IB::Straddle" do + let ( :the_option ){ IB::Option.new symbol: :Estx50, right: :put, strike: STRIKE_ESTX, expiry: IB::Option.next_expiry } + let ( :the_bag ){ IB::Symbols::Combo::stoxx_straddle } + before(:all) do + establish_connection :gateway + IB::Connection.current.activate_plugin 'spread-prototypes' + IB::Connection.current.activate_plugin 'order-prototypes' + IB::Connection.current.activate_plugin 'symbols' + IB::Connection.current.activate_plugin 'market-price' + IB::Connection.current.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + + context "fabricate with master-option" do + subject { IB::Straddle.fabricate IB::Symbols::Options.stoxx.merge( strike: STRIKE_ESTX ) } + it{ is_expected.to be_a IB::Bag } + it_behaves_like 'a valid Estx Combo' + + + end + + context "build with index underlying" do + subject{ IB::Straddle.build from: IB::Symbols::Index.stoxx, strike: STRIKE_ESTX , expiry: IB::Option.next_expiry , trading_class: 'OESX' } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + + context "build with future underlying" do + subject{ IB::Straddle.build from: IB::Symbols::Futures.es, strike: STRIKE_ES } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ES-FUT Combo' + end + + context "fabricate with stock underlying" do + subject{ IB::Straddle.fabricate IB::Symbols::Options.aapl } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid apple-stock Combo' + end + + context "build with option" do + subject{ IB::Straddle.build from: the_option, strike: STRIKE_ESTX } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end +end diff --git a/spec/ib/contracts/strangle_spec.rb b/spec/ib/contracts/strangle_spec.rb new file mode 100644 index 0000000..05ceada --- /dev/null +++ b/spec/ib/contracts/strangle_spec.rb @@ -0,0 +1,61 @@ +require 'combo_helper' +PUT_ESTX=4500 +CALL_ESTX=5000 +PUT_ES= 3400 +CALL_ES=3600 +RSpec.describe "IB::Strangle" do + let ( :the_option ){ IB::Symbols::Options.stoxx.merge strike: PUT_ESTX } + before(:all) do + establish_connection :gateway + IB::Connection.current.activate_plugin 'spread-prototypes' + IB::Connection.current.activate_plugin 'order-prototypes' + IB::Connection.current.activate_plugin 'symbols' + IB::Connection.current.activate_plugin 'market-price' + IB::Connection.current.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + context "fabricate with master-option" do + subject { IB::Strangle.fabricate the_option, 200 } + it{ is_expected.to be_a IB::Bag } + it_behaves_like 'a valid Estx Combo' + + + end + + context "build with underlying" do + subject{ IB::Strangle.build from: IB::Symbols::Index.stoxx, p: PUT_ESTX, c: CALL_ESTX } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + context "build with option" do + subject{ IB::Strangle.build from: the_option, c: CALL_ESTX } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + + + context "build with Future" do + subject{ IB::Strangle.build from: IB::Symbols::Futures.es, p: PUT_ES, c: CALL_ES } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ES-FUT Combo' + + end + + context "fabricated with FutureOption" do + subject do + fo = IB::Strangle.build( from: IB::Symbols::Futures.es, p: PUT_ES, c: CALL_ES).legs.first + IB::Strangle.fabricate fo, 200 + end + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ES-FUT Combo' + + end + +end diff --git a/spec/ib/contracts/verify_spec.rb b/spec/ib/contracts/verify_spec.rb new file mode 100644 index 0000000..86c5ae4 --- /dev/null +++ b/spec/ib/contracts/verify_spec.rb @@ -0,0 +1,50 @@ +require 'main_helper' +require 'contract_helper' + +RSpec.describe 'IB::Contract.verify' , #:if => :us_trading_hours, + :connected => true, :integration => true do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'symbols' + ib.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + + context "Verify a Stock " do + subject {IB::Symbols::Stocks.wfc.verify.first} + it{ is_expected.to be_a IB::Stock } + it_behaves_like 'a valid Contract Object' + its( :con_id ) { is_expected.not_to be_zero } + its( :contract_detail ){ is_expected.to be_a IB::ContractDetail } + its( :primary_exchange){ is_expected.to be_a String } + end + + context "Verify a Contract by is Con_ID and Smart routing" do + subject{ IB::Contract.new( con_id: 14217).verify.first } # Siemens + it{ is_expected.to be_a IB::Stock } + it_behaves_like 'a valid Contract Object' + its( :con_id ) { is_expected.not_to be_zero } + its( :contract_detail ){ is_expected.to be_a IB::ContractDetail } + its( :primary_exchange){ is_expected.to be_a String } + its( :symbol){ is_expected.to eq 'SIE' } + end + + context "Verify a Contract without SMART routing – no exchange declared – " do + subject{ IB::Contract.new con_id: 95346693 } + it{ expect( subject.verify ).to eq [] } + end + + context "Verify a Contract without SMART routing – exchange declared – " do + subject{ IB::Contract.new( con_id: 95346693, exchange: 'SGX').verify.first } + it_behaves_like 'a complete Contract Object' do + + end + end +end diff --git a/spec/ib/contracts/vertical_spec.rb b/spec/ib/contracts/vertical_spec.rb new file mode 100644 index 0000000..e4ee8dc --- /dev/null +++ b/spec/ib/contracts/vertical_spec.rb @@ -0,0 +1,56 @@ +require 'combo_helper' + +RSpec.describe "IB::Vertical" do + before(:all) do + establish_connection :gateway + IB::Connection.current.activate_plugin 'spread-prototypes' + IB::Connection.current.activate_plugin 'order-prototypes' + IB::Connection.current.activate_plugin 'symbols' +# IB::Connection.current.activate_plugin 'roll' +# IB::Connection.current.activate_plugin 'market-price' + IB::Connection.current.subscribe( :Alert ){|y| puts y.to_human } + end + + after(:all) do + close_connection + end + + + context "fabricate with master-option" do + subject { IB::Vertical.fabricate IB::Symbols::Options.stoxx , sell: 4800} + it{ is_expected.to be_a IB::Bag } + it_behaves_like 'a valid Estx Combo' + + + end + + context "build with underlying" do + subject{ IB::Vertical.build from: IB::Symbols::Index.stoxx, buy: 4800, sell: 5000, expiry: IB::Option.next_expiry } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + context "build with option" do + subject{ IB::Vertical.build from: IB::Symbols::Options.stoxx, buy: 4900 } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid Estx Combo' + end + context "build with Future" do + subject{ IB::Vertical.build from: IB::Symbols::Futures.es, buy: 5900, sell: 6100 } + + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ES-FUT Combo' + + end + + context "fabricated with FutureOption" do + subject do + fo = IB::Vertical.build( from: IB::Symbols::Futures.es, buy: 5900, sell: 6100).legs.first + IB::Vertical.fabricate fo, sell: 6200 + end + it{ is_expected.to be_a IB::Spread } + it_behaves_like 'a valid ES-FUT Combo' + + end +end diff --git a/spec/ib/extensions_spec.rb b/spec/ib/extensions_spec.rb new file mode 100644 index 0000000..417efc9 --- /dev/null +++ b/spec/ib/extensions_spec.rb @@ -0,0 +1,56 @@ +require "spec_helper" + +# Test if tweeking of basic classes works as expected + +RSpec.describe ClassExtensions do + + context "Array-Extensions" do + + end + context "Time-Extensions" do + Given( :the_time ){ Time.now } + Then{ the_time.to_ib == the_time.strftime("%Y%m%d %H:%M:%S") } + end + + # Numeric positive Values are true, zero and below is false + context "numeric boolean Test" do + Given( :the_var ){ 45 } + Then{ the_var.is_a? Numeric } + Then{ the_var.to_bool } + Given( :the_zero_var ){ 0 } + Then{ !the_zero_var.to_bool } + Given( :the_negative_var ){ -54 } + Then{ !the_zero_var.to_bool } + end + + context "string boolean Tests" do + Given( :the_var ){ "false" } + Then{ !the_var.to_bool } + Given( :the_f_var ){ "f" } + Then{ !the_var.to_bool } + Given( :the_true_var ){ "true" } + Then{ the_true_var.to_bool } + Given( :the_t_var ){ "t" } + Then{ the_true_var.to_bool } + Given( :the_1_var ){ "1" } + Then{ the_1_var.to_bool } + Given( :the_0_var ){ "0" } + Then{ !the_0_var.to_bool } + Given( :the_empty_var ){ "" } + Then{ !the_empty_var.to_bool } + Given( :the_nonempty_var ){ "not empty" } + Then{ expect{ the_nonempty_var.to_bool }.to raise_error( IB::Error ) } + end + + context "native boolean Tests" do + Given( :the_var ){ true } + Then{ the_var.to_bool } + + Given( :the_false_var ){ false } + Then{ !the_false_var.to_bool } + + Given( :the_nil_var ){ nil } + Then{ !the_nil_var.to_bool } + end + +end diff --git a/spec/ib/integration/account_info_spec.rb b/spec/ib/integration/account_info_spec.rb index cefefd7..a23ca8e 100644 --- a/spec/ib/integration/account_info_spec.rb +++ b/spec/ib/integration/account_info_spec.rb @@ -8,7 +8,7 @@ context "with subscribe option set" do before(:all) do - ib = IB::Connection.current + ib = IB::Connection.current ib.send_message :RequestAccountData, subscribe: true , account_code: ACCOUNT ib.wait_for :AccountDownloadEnd, 5 # sec end @@ -22,7 +22,7 @@ context "without subscribe option" do before(:all) do - ib = IB::Connection.current + ib = IB::Connection.current ib.send_message :RequestAccountData, account_code: ACCOUNT ib.wait_for :AccountDownloadEnd, 5 # sec end diff --git a/spec/ib/integration/fundamental_data_spec.rb b/spec/ib/integration/fundamental_data_spec.rb index f475206..27d194d 100644 --- a/spec/ib/integration/fundamental_data_spec.rb +++ b/spec/ib/integration/fundamental_data_spec.rb @@ -4,7 +4,7 @@ :connected => true, :integration => true, :reuters => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current contract = IB::Contract.new :symbol => 'IBM', @@ -33,7 +33,7 @@ it 'responds with XML with relevant data' do require 'ox' data_xml = subject.xml[:ReportSnapshot] - name = data_xml[:CoIDs][:CoID].at(1) + name = data_xml[:CoIDs][:CoID].at(1) expect(name).to match 'International Business Machines' end end diff --git a/spec/ib/messages/incoming/abstract_message_spec.rb b/spec/ib/messages/incoming/abstract_message_spec.rb index 80ffe7c..bd82e70 100644 --- a/spec/ib/messages/incoming/abstract_message_spec.rb +++ b/spec/ib/messages/incoming/abstract_message_spec.rb @@ -3,193 +3,192 @@ ## use a Message_id far beyond those defined by the tws RSpec.shared_examples_for "simple_instruction" do - it { is_expected.to be_a IB::Messages::Incoming::AbstractMessage } - its( :message_id ) { is_expected.to eq 1000 } - its( :version ) { is_expected.to eq 1 } - its( :data ) { is_expected.not_to be_empty } - its( :buffer ) { is_expected.to be_empty } -end + it { is_expected.to be_a IB::Messages::Incoming::AbstractMessage } + its( :message_id ) { is_expected.to eq 1000 } + its( :version ) { is_expected.to eq 1 } + its( :data ) { is_expected.not_to be_empty } + its( :buffer ) { is_expected.to be_empty } +end RSpec.describe IB::Messages::Incoming do - let( :simple_instruction ){ IB::Messages::Incoming.def_message 1000 } - let( :int_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_integer, :int] } - let( :string_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_string, :string] } - let( :decimal_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_decimal, :decimal] } - let( :boolean_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_bool, :boolean] } - let( :array_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_array, :array ] } - let( :hash_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_hash, :hash] } + let( :simple_instruction ){ IB::Messages::Incoming.def_message 1000 } + let( :int_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_integer, :int] } + let( :string_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_string, :string] } + let( :decimal_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_decimal, :decimal] } + let( :boolean_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_bool, :boolean] } + let( :array_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_array, :array ] } + let( :hash_instruction ){ IB::Messages::Incoming.def_message 1000, [:the_hash, :hash] } - #subject{ IB::Messages::Incoming.def_message 10 } + #subject{ IB::Messages::Incoming.def_message 10 } - context "simple Instruction" do - subject{ simple_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - end - context "Instruction with Integer" do - ## only the correct behavior implements the function. Other cases yield zero (0) - context "correct Behavior" do - subject{ int_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_integer){ is_expected.to be_a(Integer).and eq(45) } - end - context "false Integer" do - subject{ int_instruction.new ["1","zu"] } - it_behaves_like 'simple_instruction' - its(:the_integer){ is_expected.to be_a(Integer).and be_zero } - end - context "without value" do - subject{ int_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_integer){ is_expected.to be_nil } - - end - context "with Blank" do - subject{ int_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_integer){ is_expected.to be_nil } - end - end - context "Instruction with String" do - context "correct Behavior" do - subject{ string_instruction.new ["1","zu"] } - it_behaves_like 'simple_instruction' - its(:the_string){ is_expected.to be_a(String).and eq("zu") } - end - context "false Integer" do - subject{ string_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_string){ is_expected.to be_a(String).and eq("45") } - end - context "without value" do - subject{ string_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_string){ is_expected.to be_nil } - end - context "with Blank" do - subject{ string_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_string){ is_expected.to be_a(String).and be_empty } - end - end - context "Instruction with Decimal" do - context "correct Behavior" do - subject{ decimal_instruction.new ["1","3.45"] } - it_behaves_like 'simple_instruction' - its(:the_decimal){ is_expected.to be_a(BigDecimal).and eq(3.45) } - end - context "false Integer" do - subject{ decimal_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_decimal){ is_expected.to be_a(BigDecimal).and eq(45.0) } - end - context "without value" do - subject{ decimal_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_decimal){ is_expected.to be_nil } - end - context "with Blank" do - subject{ decimal_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_decimal){ is_expected.to be_nil } - end - end - context "Instruction with Boolean" do - context "correct true Behavior" do - subject{ boolean_instruction.new ["1","1"] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_truthy } - end - context "correct false Behavior" do - subject{ boolean_instruction.new ["1","0"] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_falsy } - end - context "false String" do - subject{ boolean_instruction.new ["1","Zted"] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_nil } - end - context "false Integer" do - subject{ boolean_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_nil } - end - context "without value" do - subject{ boolean_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_nil } - end - context "with Blank" do - subject{ boolean_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_bool){ is_expected.to be_nil } - end - end - context "Instruction with Array" do - context "correct empty Behavior" do - subject{ array_instruction.new ["1","0"] } - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to be_empty } - end - context "correct Behavior" do - subject{ array_instruction.new ["1","2", "eins", "2"] } - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to eq ["eins", '2'] } - end - context "false String" do - subject{ array_instruction.new ["1","Zted"] } # perhaps request nil as answer? - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to be_empty } - end - context "false Integer" do - subject{ array_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to be_an(Array).and have(45).elements} - end - context "without value" do - subject{ array_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to be_nil } - end - context "with Blank" do - subject{ array_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_array){ is_expected.to be_nil } - end - end - context "Instruction with Hash" do - context "correct empty Behavior" do - subject{ hash_instruction.new ["1","0"] } - it_behaves_like 'simple_instruction' - its(:the_hash){ is_expected.to be_empty } - end - context "correct Behavior" do - subject{ hash_instruction.new ["1","2", "eins", "2", "fuenf", "zurHeide"] } - it_behaves_like 'simple_instruction' - its(:the_hash){ is_expected.to eq :eins => '2', fuenf: 'zurHeide' } - end - context "false String" do - subject{ hash_instruction.new ["1","Zted"] } # perhaps request nil as answer? - it_behaves_like 'simple_instruction' # now: because of "abc".to_i = 0 an empty hash is created - its(:the_hash){ is_expected.to be_empty } - end - context "false Integer" do - subject{ hash_instruction.new ["1","45"] } - it_behaves_like 'simple_instruction' - its(:the_hash){ is_expected.to be_a(Hash).and be_empty} - end - context "without value" do - subject{ hash_instruction.new ["1"] } - it_behaves_like 'simple_instruction' - its(:the_hash){ is_expected.to be_nil } - end - context "with Blank" do - subject{ hash_instruction.new ["1", ""] } - it_behaves_like 'simple_instruction' - its(:the_hash){ is_expected.to be_nil } - end - end + context "simple Instruction" do + subject{ simple_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + end + context "Instruction with Integer" do + ## only the correct behavior implements the function. Other cases yield zero (0) + context "correct Behavior" do + subject{ int_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_integer){ is_expected.to be_a(Integer).and eq(45) } + end + context "false Integer" do + subject{ int_instruction.new ["1","zu"] } + it_behaves_like 'simple_instruction' + its(:the_integer){ is_expected.to be_a(Integer).and be_zero } + end + context "without value" do + subject{ int_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_integer){ is_expected.to be_nil } + end + context "with Blank" do + subject{ int_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_integer){ is_expected.to be_nil } + end + end + context "Instruction with String" do + context "correct Behavior" do + subject{ string_instruction.new ["1","zu"] } + it_behaves_like 'simple_instruction' + its(:the_string){ is_expected.to be_a(String).and eq("zu") } + end + context "false Integer" do + subject{ string_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_string){ is_expected.to be_a(String).and eq("45") } + end + context "without value" do + subject{ string_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_string){ is_expected.to be_nil } + end + context "with Blank" do + subject{ string_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_string){ is_expected.to be_a(String).and be_empty } + end + end + context "Instruction with Decimal" do + context "correct Behavior" do + subject{ decimal_instruction.new ["1","3.45"] } + it_behaves_like 'simple_instruction' + its(:the_decimal){ is_expected.to be_a(BigDecimal).and eq(3.45) } + end + context "false Integer" do + subject{ decimal_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_decimal){ is_expected.to be_a(BigDecimal).and eq(45.0) } + end + context "without value" do + subject{ decimal_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_decimal){ is_expected.to be_nil } + end + context "with Blank" do + subject{ decimal_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_decimal){ is_expected.to be_nil } + end + end + context "Instruction with Boolean" do + context "correct true Behavior" do + subject{ boolean_instruction.new ["1","1"] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_truthy } + end + context "correct false Behavior" do + subject{ boolean_instruction.new ["1","0"] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_falsy } + end + context "false String" do + subject{ boolean_instruction.new ["1","Zted"] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_nil } + end + context "false Integer" do + subject{ boolean_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_nil } + end + context "without value" do + subject{ boolean_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_nil } + end + context "with Blank" do + subject{ boolean_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_bool){ is_expected.to be_nil } + end + end + context "Instruction with Array" do + context "correct empty Behavior" do + subject{ array_instruction.new ["1","0"] } + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to be_empty } + end + context "correct Behavior" do + subject{ array_instruction.new ["1","2", "eins", "2"] } + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to eq ["eins", '2'] } + end + context "false String" do + subject{ array_instruction.new ["1","Zted"] } # perhaps request nil as answer? + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to be_empty } + end + context "false Integer" do + subject{ array_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to be_an(Array).and have(45).elements} + end + context "without value" do + subject{ array_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to be_nil } + end + context "with Blank" do + subject{ array_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_array){ is_expected.to be_nil } + end + end + context "Instruction with Hash" do + context "correct empty Behavior" do + subject{ hash_instruction.new ["1","0"] } + it_behaves_like 'simple_instruction' + its(:the_hash){ is_expected.to be_empty } + end + context "correct Behavior" do + subject{ hash_instruction.new ["1","2", "eins", "2", "fuenf", "zurHeide"] } + it_behaves_like 'simple_instruction' + its(:the_hash){ is_expected.to eq :eins => '2', fuenf: 'zurHeide' } + end + context "false String" do + subject{ hash_instruction.new ["1","Zted"] } # perhaps request nil as answer? + it_behaves_like 'simple_instruction' # now: because of "abc".to_i = 0 an empty hash is created + its(:the_hash){ is_expected.to be_empty } + end + context "false Integer" do + subject{ hash_instruction.new ["1","45"] } + it_behaves_like 'simple_instruction' + its(:the_hash){ is_expected.to be_a(Hash).and be_empty} + end + context "without value" do + subject{ hash_instruction.new ["1"] } + it_behaves_like 'simple_instruction' + its(:the_hash){ is_expected.to be_nil } + end + context "with Blank" do + subject{ hash_instruction.new ["1", ""] } + it_behaves_like 'simple_instruction' + its(:the_hash){ is_expected.to be_nil } + end + end end diff --git a/spec/ib/messages/incoming/account_info_spec.rb b/spec/ib/messages/incoming/account_info_spec.rb index 2260ea0..20d8ce8 100644 --- a/spec/ib/messages/incoming/account_info_spec.rb +++ b/spec/ib/messages/incoming/account_info_spec.rb @@ -1,17 +1,17 @@ require 'account_helper' RSpec.shared_examples 'Portfolio Value Message' do - subject{ the_portfolio_value } + subject{ the_portfolio_value } it { is_expected.to be_an IB::Messages::Incoming::PortfolioValue } its( :message_type ){ is_expected.to eq :PortfolioValue } - its( :contract ){ is_expected.to be_a IB::Contract } - its( :portfolio_value ){ is_expected.to be_a IB::PortfolioValue } + its( :contract ){ is_expected.to be_a IB::Contract } + its( :portfolio_value ){ is_expected.to be_a IB::PortfolioValue } its( :message_id ){ is_expected.to eq 7 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } - it_behaves_like 'Valid PortfolioValue Object' do - let( :the_portfolio_value_object ){ the_portfolio_value.portfolio_value } - end + it_behaves_like 'Valid PortfolioValue Object' do + let( :the_portfolio_value_object ){ the_portfolio_value.portfolio_value } + end it 'has class accessors as well' do expect( subject.class.message_id).to eq 7 expect( subject.class.message_type).to eq :PortfolioValue @@ -19,24 +19,24 @@ end RSpec.shared_examples_for 'Valid PortfolioValue Object' do - subject{ the_portfolio_value_object } - it{ is_expected.to be_a IB::PortfolioValue } - its( :position ) { is_expected.to be_a BigDecimal } - its( :market_price ) { is_expected.to be_a BigDecimal } - its( :market_value ) { is_expected.to be_a BigDecimal } - its( :average_cost ) { is_expected.to be_a BigDecimal } - its( :unrealized_pnl ) { is_expected.to be_a BigDecimal } - its( :realized_pnl ) { is_expected.to be_a BigDecimal } + subject{ the_portfolio_value_object } + it{ is_expected.to be_a IB::PortfolioValue } + its( :position ) { is_expected.to be_a BigDecimal } + its( :market_price ) { is_expected.to be_a BigDecimal } + its( :market_value ) { is_expected.to be_a BigDecimal } + its( :average_cost ) { is_expected.to be_a BigDecimal } + its( :unrealized_pnl ) { is_expected.to be_a BigDecimal } + its( :realized_pnl ) { is_expected.to be_a BigDecimal } end RSpec.shared_examples 'Account Value Message' do - subject{ the_account_value } + subject{ the_account_value } it { is_expected.to be_an IB::Messages::Incoming::AccountValue } its( :message_type ){ is_expected.to eq :AccountValue } - its( :account_name ){ is_expected.to be_a String } + its( :account_name ){ is_expected.to be_a String } its( :message_id ){ is_expected.to eq 6 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 6 @@ -48,28 +48,26 @@ context 'Message received from IB' do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - ib.send_message :RequestAccountData, :subscribe => true, :account_code => ACCOUNT - + ib.send_message :RequestAccountData, :subscribe => true, :account_code => ACCOUNT ib.wait_for :PortfolioValue - sleep 1 - ib.send_message :RequestAccountData, :subscribe => false + sleep 1 + ib.send_message :RequestAccountData, :subscribe => false end after(:all) { close_connection } - - it_behaves_like 'Portfolio Value Message' do - let( :the_portfolio_value ){ IB::Connection.current.received[:PortfolioValue].first } - end + it_behaves_like 'Portfolio Value Message' do + let( :the_portfolio_value ){ IB::Connection.current.received[:PortfolioValue].first } + end - it_behaves_like 'Account Value Message' do - let( :the_account_value ) { IB::Connection.current.received[:AccountValue].first } - end + it_behaves_like 'Account Value Message' do + let( :the_account_value ) { IB::Connection.current.received[:AccountValue].first } + end - it_behaves_like 'Valid AccountValue Object' do - let( :the_account_value_object ){ IB::Connection.current.received[:AccountValue].first.account_value } - end + it_behaves_like 'Valid AccountValue Object' do + let( :the_account_value_object ){ IB::Connection.current.received[:AccountValue].first.account_value } + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/account_summary_spec.rb b/spec/ib/messages/incoming/account_summary_spec.rb index d391552..2bc5736 100644 --- a/spec/ib/messages/incoming/account_summary_spec.rb +++ b/spec/ib/messages/incoming/account_summary_spec.rb @@ -4,8 +4,8 @@ it { is_expected.to be_an IB::Messages::Incoming::AccountSummary } its( :message_type){ is_expected.to eq :AccountSummary } its( :message_id ){ is_expected.to eq 63 } - its( :request_id ){is_expected.to be_a Integer} - its( :buffer ){ is_expected.to be_empty } + its( :request_id ){is_expected.to be_a Integer} + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id ).to eq 63 @@ -19,28 +19,27 @@ context 'Message received from IB' do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - req_id= ib.send_message :RequestAccountSummary, tags: 'RegTMargin,ExcessLiquidity, DayTradesRemaining' - + req_id= ib.send_message :RequestAccountSummary, tags: 'RegTMargin,ExcessLiquidity, DayTradesRemaining' ib.wait_for :AccountSummary - sleep 1 - ib.send_message :CancelAccountSummary, id: req_id + sleep 1 + ib.send_message :CancelAccountSummary, id: req_id end after(:all) { close_connection } subject { IB::Connection.current.received[:AccountSummary].first } - + it_behaves_like 'AccountSummary message' - it_behaves_like 'Valid AccountValue Object' do - let( :the_account_value_object ){ IB::Connection.current.received[:AccountSummary].first.account_value } - end - it "has appropiate attributes" do - expect( subject.account_value ).to be_a IB::AccountValue - expect( subject.account_name ).to be_a String - end + it_behaves_like 'Valid AccountValue Object' do + let( :the_account_value_object ){ IB::Connection.current.received[:AccountSummary].first.account_value } + end + it "has appropiate attributes" do + expect( subject.account_value ).to be_a IB::AccountValue + expect( subject.account_name ).to be_a String + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/account_update_multi_spec.rb b/spec/ib/messages/incoming/account_update_multi_spec.rb index b1bafae..c5b89b5 100644 --- a/spec/ib/messages/incoming/account_update_multi_spec.rb +++ b/spec/ib/messages/incoming/account_update_multi_spec.rb @@ -1,18 +1,18 @@ require 'main_helper' -RSpec.shared_examples 'Account Updates Multi Message' do +RSpec.shared_examples 'Account Updates Multi Message' do it { is_expected.to be_an IB::Messages::Incoming::AccountUpdatesMulti } its(:message_type) { is_expected.to eq :AccountUpdatesMulti } - its( :value ){ is_expected.to be_a BigDecimal } - its( :key ){ is_expected.to be_a String } - its( :currency ){ is_expected.to be_a( String ).or be_nil } + its( :value ){ is_expected.to be_a Numeric } + its( :key ){ is_expected.to be_a String } + its( :currency ){ is_expected.to be_a( String ).or be_nil } its(:message_id) { is_expected.to eq 73 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 73 expect( subject.class.message_type).to eq :AccountUpdatesMulti - puts subject.inspect + puts subject.inspect end end @@ -21,20 +21,18 @@ context 'Message received wfrom IB' do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - request_id =ib.send_message :RequestAccountUpdatesMulti #, account: 'ALL' is default + request_id =ib.send_message :RequestAccountUpdatesMulti #, account: 'ALL' is default ib.wait_for :AccountUpdatesMulti, 10 - sleep 0.1 - ib.send_message :CancelAccountUpdatesMulti, request_id: request_id + sleep 0.1 + ib.send_message :CancelAccountUpdatesMulti, request_id: request_id end after(:all) { close_connection } - subject{ IB::Connection.current.received[:AccountUpdatesMulti].first } - it_behaves_like 'Account Updates Multi Message' - - + subject{ IB::Connection.current.received[:AccountUpdatesMulti].first } + it_behaves_like 'Account Updates Multi Message' end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/alert_spec.rb b/spec/ib/messages/incoming/alert_spec.rb index 8e57901..9a560ec 100644 --- a/spec/ib/messages/incoming/alert_spec.rb +++ b/spec/ib/messages/incoming/alert_spec.rb @@ -11,8 +11,8 @@ its( :error_id ){ is_expected.to eq -1 } its( :code ){ is_expected.to be_between( 2104, 2107 ) } its( :message ){ is_expected.to match /data farm/ } - ## either "Market data farm connection is OK:cashfarm " - ## or "HMDS data farm connection is inactive but should be available upon demand.euhmds" + ## either "Market data farm connection is OK:cashfarm " + ## or "HMDS data farm connection is inactive but should be available upon demand.euhmds" its(:to_human) { is_expected.to match /TWS Warning/ } it 'has class accessors as well' do @@ -39,7 +39,7 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection sleep 0.1 # wait for alerts to appear # puts IB::Connection.current.received.inspect # in case of failing tests look for alert messasges end @@ -47,7 +47,7 @@ after(:all) { close_connection } subject { IB::Connection.current.received[:Alert].first } - + it_behaves_like 'this Alert message' end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/contract_data_spec.rb b/spec/ib/messages/incoming/contract_data_spec.rb index 7050403..25bc929 100644 --- a/spec/ib/messages/incoming/contract_data_spec.rb +++ b/spec/ib/messages/incoming/contract_data_spec.rb @@ -4,40 +4,39 @@ RSpec.describe IB::Messages::Incoming::ContractData do - before(:all) do - establish_connection - end + before(:all) do + establish_connection + end - after(:all) { close_connection } + after(:all) { close_connection } context IB::Stock do before(:all) do ib = IB::Connection.current - ib.send_message :RequestContractDetails, contract: IB::Stock.new( symbol: 'GE', currency: 'USD', exchange:'SMART' ) - ib.wait_for :ContractDetailsEnd + ib.send_message :RequestContractDetails, contract: IB::Stock.new( symbol: 'GE', currency: 'USD', exchange:'SMART' ) + ib.wait_for :ContractDetailsEnd, :ContractDataEnd end - after(:all){ IB::Connection.current.clear_received :ContractData } - - -# it_behaves_like 'ContractData Message' do -# let( :the_message ){ IB::Connection.current.received[:ContractData].first } -# end - context "Basics" do - subject{ IB::Connection.current.received[:ContractData].contract.last } - - it_behaves_like 'a complete Contract Object' - its( :sec_type ){is_expected.to eq :stock} - its( :symbol ){is_expected.to eq 'GE'} - its( :con_id ){is_expected.to eq 7516} - end - - context "received a single contract" do - subject{ IB::Connection.current.received[:ContractData] } - it{ is_expected.to be_a Array } - its(:size){is_expected.to eq 1 } - end - end + after(:all){ IB::Connection.current.clear_received :ContractDetails } + +# it_behaves_like 'ContractData Message' do +# let( :the_message ){ IB::Connection.current.received[:ContractData].first } +# end + context "Basics" do + subject{ IB::Connection.current.received[:ContractDetails].contract.last } + + it_behaves_like 'a complete Contract Object' + its( :sec_type ){is_expected.to eq :stock} + its( :symbol ){is_expected.to eq 'GE'} + its( :con_id ){is_expected.to eq 498843743} + end + + context "received a single contract" do + subject{ IB::Connection.current.received[:ContractDetails] } + it{ is_expected.to be_a Array } + its(:size){is_expected.to eq 1 } + end + end end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/head_time_stamp_spec.rb b/spec/ib/messages/incoming/head_time_stamp_spec.rb index 5aca804..e470667 100644 --- a/spec/ib/messages/incoming/head_time_stamp_spec.rb +++ b/spec/ib/messages/incoming/head_time_stamp_spec.rb @@ -5,7 +5,7 @@ it { is_expected.to be_an IB::Messages::Incoming::HeadTimeStamp } its(:message_type) { is_expected.to eq :HeadTimeStamp } its(:message_id) { is_expected.to eq 88 } - its(:request_id) {is_expected.to eq 123} + its(:request_id) {is_expected.to eq 123} its(:date) { is_expected.to be_a Time } its(:to_human) { is_expected.to match /First Historical Datapoint/ } @@ -33,15 +33,15 @@ context "Message for #{SAMPLE.to_human} received from IB", :connected => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - ib.send_message :RequestHeadTimeStamp, request_id: 123, contract: SAMPLE # IB::Stock.new(symbol: 'GE') + ib.send_message :RequestHeadTimeStamp, request_id: 123, contract: SAMPLE # IB::Stock.new(symbol: 'GE') ib.wait_for :HeadTimeStamp, 3 end after(:all) { close_connection } subject { IB::Connection.current.received[:HeadTimeStamp].first } - + it_behaves_like 'HeadTimeStamp message' end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/histogram_data_spec.rb b/spec/ib/messages/incoming/histogram_data_spec.rb index 40c12e0..fd53a70 100644 --- a/spec/ib/messages/incoming/histogram_data_spec.rb +++ b/spec/ib/messages/incoming/histogram_data_spec.rb @@ -4,10 +4,10 @@ it { is_expected.to be_an IB::Messages::Incoming::HistogramData } its(:message_type) { is_expected.to eq :HistogramData } its(:message_id) { is_expected.to eq 89 } - its(:request_id) {is_expected.to eq 119} + its(:request_id) {is_expected.to eq 119} its(:number_of_points) { is_expected.to be > 0 } its(:results) { is_expected.to be_an Array } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 89 @@ -29,24 +29,24 @@ context 'Message received from IB', :connected => true do ## This happends on the lack of permissions #I, [2018-03-02T05:44:38.411662 #15045] - #INFO -- : TWS Warning 10188: Failed to request histogram data:No market data permissions for ISLAND STK + #INFO -- : TWS Warning 10188: Failed to request histogram data:No market data permissions for ISLAND STK # ## before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - if OPTS[:market_data] - ib.send_message :RequestHistogramData, contract: SAMPLE, - time_period: '1 week', request_id: 119 + if OPTS[:market_data] + ib.send_message :RequestHistogramData, contract: SAMPLE, + time_period: '1 week', request_id: 119 ib.wait_for :HistogramData - end + end end after(:all) { close_connection } subject { IB::Connection.current.received[:HistogramData].first } - + it_behaves_like 'HistogramData message' if OPTS[:marke_data] end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/historical_data_spec.rb b/spec/ib/messages/incoming/historical_data_spec.rb index f3032a8..4bd4b41 100644 --- a/spec/ib/messages/incoming/historical_data_spec.rb +++ b/spec/ib/messages/incoming/historical_data_spec.rb @@ -4,10 +4,10 @@ it { is_expected.to be_an IB::Messages::Incoming::HistoricalData } its(:message_type) { is_expected.to eq :HistoricalData } its(:message_id) { is_expected.to eq 17 } - its(:request_id) {is_expected.to eq 123} - its( :count ){ is_expected.to be_a Integer } + its(:request_id) {is_expected.to eq 123} + its( :count ){ is_expected.to be_a Integer } its(:start_date) { is_expected.to be_a DateTime } - its(:end_date) { is_expected.to be_a DateTime } + its(:end_date) { is_expected.to be_a DateTime } it 'has class accessors as well' do expect( subject.class.message_id).to eq 17 @@ -25,21 +25,21 @@ context 'Instantiated with buffer data' do subject do IB::Messages::Incoming::HistoricalData.new ["123", "20181120 17:53:13", "20181121 17:53:13", "9", - "1542787200", "3124.60", "3144.37", "3124.60", "3144.36", "0", "0", "223", - "1542790800", "3144.36", "3145.60", "3134.06", "3138.18", "0", "0", "218", - "1542794400", "3138.19", "3148.22", "3128.99", "3131.11", "0", "0", "224", - "1542798000", "3131.11", "3137.16", "3127.16", "3131.37", "0", "0", "218", - "1542801600", "3131.38", "3142.15", "3129.36", "3141.27", "0", "0", "210", - "1542805200", "3141.27", "3143.85", "3136.37", "3140.03", "0", "0", "211", - "1542808800", "3140.03", "3143.74", "3133.66", "3141.54", "0", "0", "225", - "1542812400", "3141.54", "3152.21", "3141.54", "3147.17", "0", "0", "222", - "1542816000", "3147.18", "3156.77", "3145.27", "3156.77", "0", "0", "108"] - end + "1542787200", "3124.60", "3144.37", "3124.60", "3144.36", "0", "0", "223", + "1542790800", "3144.36", "3145.60", "3134.06", "3138.18", "0", "0", "218", + "1542794400", "3138.19", "3148.22", "3128.99", "3131.11", "0", "0", "224", + "1542798000", "3131.11", "3137.16", "3127.16", "3131.37", "0", "0", "218", + "1542801600", "3131.38", "3142.15", "3129.36", "3141.27", "0", "0", "210", + "1542805200", "3141.27", "3143.85", "3136.37", "3140.03", "0", "0", "211", + "1542808800", "3140.03", "3143.74", "3133.66", "3141.54", "0", "0", "225", + "1542812400", "3141.54", "3152.21", "3141.54", "3147.17", "0", "0", "222", + "1542816000", "3147.18", "3156.77", "3145.27", "3156.77", "0", "0", "108"] + end it_behaves_like 'HistoricalData message' - its( :count ){ is_expected.to eq 9 } - its( :results ){ is_expected.to be_a Array } - its( :results ){ is_expected.to have(9).bars } + its( :count ){ is_expected.to eq 9 } + its( :results ){ is_expected.to be_a Array } + its( :results ){ is_expected.to have(9).bars } end @@ -47,26 +47,26 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - if OPTS[:market_data] - ib.send_message IB::Messages::Outgoing::RequestHistoricalData.new( + if OPTS[:market_data] + ib.send_message IB::Messages::Outgoing::RequestHistoricalData.new( :request_id => 123, :contract => SAMPLE, #IB::Symbols::Forex.gbpusd , :end_date_time => Time.now.to_ib, :duration => '1 D', # :bar_size => :hour1, # IB::BAR_SIZES.key(:hour)? :what_to_show => :trades, - :use_rth => 0, - :keep_up_todate => 0,) + :use_rth => 0, + :keep_up_todate => 0,) ib.wait_for :HistoricalData - end + end end after(:all) { close_connection } subject { IB::Connection.current.received[:HistoricalData].last } - + it_behaves_like 'HistoricalData message' if OPTS[:market_data] diff --git a/spec/ib/messages/incoming/managed_accounts_spec.rb b/spec/ib/messages/incoming/managed_accounts_spec.rb index 89a6858..2ba4924 100644 --- a/spec/ib/messages/incoming/managed_accounts_spec.rb +++ b/spec/ib/messages/incoming/managed_accounts_spec.rb @@ -4,11 +4,11 @@ it { is_expected.to be_an IB::Messages::Incoming::ManagedAccounts } its(:message_type) { is_expected.to eq :ManagedAccounts } its(:message_id) { is_expected.to eq 15 } - its(:accounts) {is_expected.to be_an Array} - its( :buffer ){ is_expected.to be_empty } + its(:accounts) {is_expected.to be_an Array} + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do - expect( subject.class.message_id).to eq 15 + expect( subject.class.message_id).to eq 15 expect( subject.class.message_type).to eq :ManagedAccounts end end @@ -19,17 +19,17 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection end after(:all) { close_connection } subject { IB::Connection.current.received[:ManagedAccounts].first } - + it_behaves_like 'ManagedAccounts message' - it_behaves_like 'Valid Account Object' do - let( :the_account_object ){ IB::Connection.current.received[:ManagedAccounts].first.accounts.first } - end + it_behaves_like 'Valid Account Object' do + let( :the_account_object ){ IB::Connection.current.received[:ManagedAccounts].first.accounts.first } + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/open_position_spec.rb b/spec/ib/messages/incoming/open_position_spec.rb new file mode 100644 index 0000000..ee86dcf --- /dev/null +++ b/spec/ib/messages/incoming/open_position_spec.rb @@ -0,0 +1,212 @@ +require 'main_helper' + +RSpec.shared_examples 'Open Position Message' do + subject{ the_message } + it { is_expected.to be_an IB::Messages::Incoming::OpenOrder } + its( :message_type) { is_expected.to eq :OpenOrder } + its( :contract ) { is_expected.to be_a IB::Contract } + its( :message_id ) { is_expected.to eq 5 } + its( :client_id ) { is_expected.to eq 2000 } + its( :buffer ) { is_expected.to be_empty } + + it 'has class accessors as well' do + expect( subject.class.message_id).to eq 5 + expect( subject.class.message_type).to eq :OpenOrder + end +end +RSpec.shared_examples 'Standard Limit Order' do + # covers most of attributes filled directly through `def_message` + subject{ the_message.order } + + its( :order_type ) { is_expected.to eq :limit } + its( :aux_price ) { is_expected.to be_zero } + its( :tif ) { is_expected.to eq :good_till_cancelled } # todo test other states, too + its( :oca_group ) { is_expected.to be_empty } + its( :good_after_time ) { is_expected.to be_empty } + its( :good_till_date ) { is_expected.to be_empty } + its( :account ) { is_expected.to match /\w\d/ } + its( :open_close ) { is_expected.to eq :open } + its( :origin ) { is_expected.to eq :customer } + its( :order_ref ) { is_expected.to be_empty } # customer defined reference + its( :client_id ) { is_expected.to eq 2000 } +# its( :perm_id ) { is_expected.to match /\d{7,9}/ } + its( :outside_rth ) { is_expected.to be_falsey } + its( :hidden ) { is_expected.to be_falsey } + its( :discretionary_amount ) { is_expected.to be_zero } + + its( :fa_group ) { is_expected.to be_empty } + its( :fa_method ) { is_expected.to be_empty } + its( :fa_percentage ) { is_expected.to be_empty } + its( :fa_profile ) { is_expected.to be_empty } + + its( :model_code ) { is_expected.to be_empty } + its( :rule_80a ) { is_expected.to be_nil } + its( :percent_offset ) { is_expected.to be_empty } + its( :settling_firm ) { is_expected.to be_empty } + its( :short_sale_slot ) { is_expected.to eq :default } + its( :designated_location ) { is_expected.to be_empty } + its( :exempt_code ) { is_expected.to eq -1 } + its( :auction_strategy ) { is_expected.to eq :none } + its( :starting_price ) { is_expected.to be_nil } + its( :stock_ref_price ) { is_expected.to be_empty } + its( :delta ) { is_expected.to be_empty } + its( :stock_range_lower ) { is_expected.to be_empty } + its( :stock_range_upper ) { is_expected.to be_empty } + its( :display_size ) { is_expected.to be_nil } ### unset if MAX_INT is transmitted + its( :block_order ) { is_expected.to be_falsey } + its( :sweep_to_fill ) { is_expected.to be_falsey } + its( :all_or_none ) { is_expected.to be_falsey} + its( :min_quantity ) { is_expected.to be_empty } + its( :oca_type) { is_expected.to eq :reduce_no_block } # 3 + # etrade + its( :firm_quote_only ) { is_expected.to be_falsey } + its( :nbbo_price_cap ) { is_expected.to be_empty } + its( :parent_id ) { is_expected.to be_zero } + its( :trigger_method ) { is_expected.to eq :default } + its( :volatility ) { is_expected.to be_nil } + its( :volatility_type ) { is_expected.to eq :annual } + its( :delta_neutral_order_type ){ is_expected.to eq :none } # see constants#210 + its( :delta_neutral_aux_price ) { is_expected.to be_nil } +end + + +RSpec.shared_examples 'Extended Limit Order' do + # covers attributes filled through load_map + subject{ the_message.order } + + its( :continuous_update ) { is_expected.to be_zero } + its( :reference_price_type ) { is_expected.to be_nil } + its( :trail_stop_price ) { is_expected.to be_empty } + its( :trailing_percent ) { is_expected.to be_empty } + its( :basis_points ) { is_expected.to be_nil } + its( :basis_points_type ) { is_expected.to be_nil } + + its( :leg_prices ) { is_expected.to be_a Array } + its( :leg_prices ) { is_expected.to be_empty } + its( :combo_params ) { is_expected.to be_a Array } # todo Needs testing with combo_params + # should be a Hash, ... support.rb --> read_hash + its( :combo_params ) { is_expected.to be_empty } + + its( :scale_init_level_size ) { is_expected.to be_nil } + its( :scale_subs_level_size ) { is_expected.to be_nil } + its( :scale_price_increment ) { is_expected.to be_nil } + + its( :hedge_type ) { is_expected.to be_nil } + its( :opt_out_smart_routing ) { is_expected.to be false } + its( :clearing_account ) { is_expected.to be_empty } + its( :clearing_intent ) { is_expected.to eq :ib } + its( :not_held ) { is_expected.to be false } + its( :algo_strategy ) { is_expected.to be_empty } + its( :solicided ) { is_expected.to be false } + its( :what_if ) { is_expected.to be false } + its( :random_size ) { is_expected.to be false } + its( :random_price ) { is_expected.to be false } + its( :conditions ) { is_expected.to be_empty } + + its( :adjusted_order_type ) { is_expected.to eq "None" } + its( :trigger_price ) { is_expected.to be_nil } + its( :trail_stop_price ) { is_expected.to be_empty } + its( :limit_price_offset ) { is_expected.to be_nil } + its( :adjusted_stop_price ) { is_expected.to be_nil } + its( :adjusted_stop_limit_price ) { is_expected.to be_nil } + its( :adjusted_trailing_amount ) { is_expected.to be_nil } + its( :adjustable_trailing_unit ) { is_expected.to be_zero } # only integer allowed + + its( :soft_dollar_tier_name ) { is_expected.to be_empty } + its( :soft_dollar_tier_value ) { is_expected.to be_empty } + its( :soft_dollar_tier_display_name ) { is_expected.to be_empty } + + its( :cash_qty ) { is_expected.to be_zero } # 0.0 + its( :dont_use_auto_price_for_hedge ) { is_expected.to be true } + its( :is_O_ms_container ) { is_expected.to be false } + its( :discretionary_up_to_limit_price ) { is_expected.to be false } + its( :use_price_management_algo ) { is_expected.to be false } + its( :duration ) { is_expected.to be_nil } + its( :post_to_ats ) { is_expected.to be_nil } + its( :auto_cancel_parent) { is_expected.to be false } + +end + +RSpec.shared_examples 'Extended OrderState attributes' do + # covers attributes filled through load_map + subject{ the_message.order_state } + its( :status ) { is_expected.to eq "Submitted" } # OrderState attributes are nil + its( :init_margin_before ) { is_expected.to be_nil } # if what_if is not set + its( :maint_margin_before ) { is_expected.to be_nil } + its( :equity_with_loan_before ) { is_expected.to be_nil } + its( :init_margin_change ) { is_expected.to be_nil } # if what_if is not set + its( :maint_margin_change ) { is_expected.to be_nil } + its( :equity_with_loan_change ) { is_expected.to be_nil } + its( :init_margin_after ) { is_expected.to be_nil } # if what_if is not set + its( :maint_margin_after ) { is_expected.to be_nil } + its( :equity_with_loan_after ) { is_expected.to be_nil } + its( :commission ) { is_expected.to be_nil } + its( :min_commission ) { is_expected.to be_nil } + its( :max_commission ) { is_expected.to be_nil } + its( :commission_currency ) { is_expected.to be_empty } + its( :warning_text ) { is_expected.to be_empty } + +end + +RSpec.shared_examples 'empty Combo Order attributes' do + # covers attributes filled through load_map + subject{ the_message.contract } + its( :legs_description ) { is_expected.to be_empty } + its( :combo_legs ) { is_expected.to be_a Array } + its( :combo_legs ) { is_expected.to be_empty } +end +RSpec.describe IB::Messages::Incoming::OpenOrder do + + context "Syntetic Message" do + let( :the_message ) do + IB::Messages::Incoming::OpenOrder.new( + ["4", "14217", "SIE", "STK", "", "0", "?", "", "SMART", "EUR", "SIE", "XETRA", "BUY", "1", "LMT", "70.0", "0.0", "GTC", "", "DU4035275", "", "0", "", "2000", "727847514", "0", "0", "0", "", "", "", "", "", "", "", "", "0", "", "", "0", "", "-1", "0", "", "", "", "", "", "2147483647", "0", "0", "0", "", "3", "0", "0", "", "0", "0", "", "0", "None", "", "0", "", "", "", "?", "0", "0", "", "0", "0", "", "", "", "", "", "0", "0", "0", "2147483647", "2147483647", "", "", "0", "", "IB", "0", "0", "", "0", "0", "Submitted", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "", "", "", "", "", "0", "0", "0", "None", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "1.7976931348623157E308", "0", "", "", "", "0", "1", "0", "0", "0", "", "", "0"] + ## trailing_unit | + ##cash_qty +) + end + + it "has the basic attributes" do + expect( the_message.local_id ).to eq 4 + expect( the_message.contract.symbol ).to eq 'SIE' + puts the_message.inspect + end + it "references to the right contract" do + siemens = IB::Stock.new symbol: 'SIE', exchange: 'SMART', currency: 'EUR', + exchange: 'XETRA' + expect( the_message.contract ).to eq siemens + end + + it "references to the correct order" do + expect( the_message.order.perm_id ).to eq 727847514 + expect( the_message.order.action ).to eq :buy + expect( the_message.order.total_quantity ).to eq 1 + expect( the_message.order.limit_price ).to eq 70 + end + it_behaves_like 'Open Position Message' + it_behaves_like 'Standard Limit Order' + it_behaves_like 'Extended OrderState attributes' + it_behaves_like 'Extended Limit Order' + it_behaves_like 'empty Combo Order attributes' + + end +# context 'Message received from IB', :connected => true do +# before(:all) do +# establish_connection +# ib = IB::Connection.current +# ib.send_message :RequestPositionsMulti, request_id: 204, account: ACCOUNT +# ib.wait_for :PositionsMulti, 10 +# sleep 1 +# ib.send_message :CancelPositionsMulti, :subscribe => false +# end +# +# after(:all) { close_connection } +# +# it_behaves_like 'Position Message' do +# let( :the_message ){ IB::Connection.current.received[:PositionsMulti].first } +# end +# + +# end # +end # describe IB::Messages:Incoming + diff --git a/spec/ib/messages/incoming/option_chain_spec.rb b/spec/ib/messages/incoming/option_chain_spec.rb index 06dd5d4..44930bd 100644 --- a/spec/ib/messages/incoming/option_chain_spec.rb +++ b/spec/ib/messages/incoming/option_chain_spec.rb @@ -26,11 +26,10 @@ before(:all) do establish_connection ib = IB::Connection.current - the_con_id = SAMPLE.con_id.presence || request_con_id.first - ib.send_message :RequestOptionChainDefinition, con_id: the_con_id, + ib.send_message :RequestOptionChainDefinition, con_id: SAMPLE.con_id, symbol: SAMPLE.symbol, - # exchange: 'BOX,CBOE', + # exchange: 'BOX,CBOE', sec_type: "STK" #contract.sec_type diff --git a/spec/ib/messages/incoming/order_status_spec.rb b/spec/ib/messages/incoming/order_status_spec.rb index 4be7bd5..d09e2fa 100644 --- a/spec/ib/messages/incoming/order_status_spec.rb +++ b/spec/ib/messages/incoming/order_status_spec.rb @@ -8,7 +8,7 @@ its( :buffer ) { is_expected.to be_empty } # Work on openOrder-Message has to be finished. its( :local_id ) { is_expected.to be_an Integer } its( :status ) { is_expected.to match /Submit/ } - ## to do: evaluate! + ## to do: evaluate! # its( :to_human ) { is_expected.to match // } it 'has proper order_state accessor' do @@ -28,21 +28,21 @@ it 'has class accessors as well' do expect( subject.class.message_id).to eq 3 expect( subject.class.version).to be_zero - expect( subject.class.message_type).to eq :OrderStatus + expect( subject.class.message_type).to eq :OrderStatus end end describe IB::Messages::Incoming::OrderStatus do - context 'Instantiated with raw data' do - subject do - IB::Messages::Incoming::OrderStatus.new [ - "3", "Submitted", "0", "100", "0", "2044311842", "0", "0", "1111", "", "0" ] + context 'Instantiated with raw data' do + subject do + IB::Messages::Incoming::OrderStatus.new [ + "3", "Submitted", "0", "100", "0", "2044311842", "0", "0", "1111", "", "0" ] - end - it_behaves_like 'OrderStatus message' - end + end + it_behaves_like 'OrderStatus message' + end context 'Instantiated with data Hash' do subject do IB::Messages::Incoming::OrderStatus.new :version => 0, diff --git a/spec/ib/messages/incoming/position_data_spec.rb b/spec/ib/messages/incoming/position_data_spec.rb index e038464..932633f 100644 --- a/spec/ib/messages/incoming/position_data_spec.rb +++ b/spec/ib/messages/incoming/position_data_spec.rb @@ -3,9 +3,9 @@ shared_examples_for 'PositionData message' do it { is_expected.to be_an IB::Messages::Incoming::PositionData } its(:message_type) { is_expected.to eq :PositionData } - its( :contract ){ is_expected.to be_a IB::Contract } + its( :contract ){ is_expected.to be_a IB::Contract } its(:message_id) { is_expected.to eq 61 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 61 @@ -19,20 +19,20 @@ # ## before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - ib.send_message :RequestPositions - + ib.send_message :RequestPositions + ib.wait_for :PositionData - sleep 1 - ib.send_message :CancelPositions + sleep 1 + ib.send_message :CancelPositions end after(:all) { close_connection } subject { IB::Connection.current.received[:PositionData].first } - + it_behaves_like 'PositionData message' end # diff --git a/spec/ib/messages/incoming/positios_multi_spec.rb b/spec/ib/messages/incoming/positios_multi_spec.rb index 14e8095..f5a63dc 100644 --- a/spec/ib/messages/incoming/positios_multi_spec.rb +++ b/spec/ib/messages/incoming/positios_multi_spec.rb @@ -1,12 +1,12 @@ require 'main_helper' -RSpec.shared_examples 'Position Message' do - subject{ the_message } +RSpec.shared_examples 'Position Message' do + subject{ the_message } it { is_expected.to be_an IB::Messages::Incoming::PositionsMulti } its(:message_type) { is_expected.to eq :PositionsMulti } - its( :contract ){ is_expected.to be_a IB::Contract } + its( :contract ){ is_expected.to be_a IB::Contract } its(:message_id) { is_expected.to eq 71 } - its( :buffer ){ is_expected.to be_empty } + its( :buffer ){ is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 71 @@ -16,35 +16,35 @@ RSpec.describe IB::Messages::Incoming::PositionsMulti do - context "Syntetic Message" do - let( :the_message ) do - IB::Messages::Incoming::PositionsMulti.new( - ["1", "204", "DU167348", "14171", "LHA", "STK", "", "0.0", "", "", "IBIS", "EUR", "LHA", "XETRA", "10124", "15.39373125"]) - end + context "Syntetic Message" do + let( :the_message ) do + IB::Messages::Incoming::PositionsMulti.new( + ["1", "204", "DU167348", "14171", "LHA", "STK", "", "0.0", "", "", "IBIS", "EUR", "LHA", "XETRA", "10124", "15.39373125"]) + end - it "has the basic attributes" do - expect( the_message.request_id ).to eq 204 - expect( the_message.contract.symbol ).to eq 'LHA' - puts the_message.inspect - end - it_behaves_like 'Position Message' + it "has the basic attributes" do + expect( the_message.request_id ).to eq 204 + expect( the_message.contract.symbol ).to eq 'LHA' + puts the_message.inspect + end + it_behaves_like 'Position Message' - end + end context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - ib.send_message :RequestPositionsMulti, request_id: 204, account: ACCOUNT + ib.send_message :RequestPositionsMulti, request_id: 204, account: ACCOUNT ib.wait_for :PositionsMulti, 10 - sleep 1 - ib.send_message :CancelPositionsMulti, :subscribe => false + sleep 1 + ib.send_message :CancelPositionsMulti, :subscribe => false end after(:all) { close_connection } - - it_behaves_like 'Position Message' do - let( :the_message ){ IB::Connection.current.received[:PositionsMulti].first } - end + + it_behaves_like 'Position Message' do + let( :the_message ){ IB::Connection.current.received[:PositionsMulti].first } + end end # diff --git a/spec/ib/messages/incoming/receive_fa_spec.rb b/spec/ib/messages/incoming/receive_fa_spec.rb index 3cef9ad..e6722fd 100644 --- a/spec/ib/messages/incoming/receive_fa_spec.rb +++ b/spec/ib/messages/incoming/receive_fa_spec.rb @@ -7,10 +7,10 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - - ib.send_message :RequestFA, fa_data_type: 3 # alias + + ib.send_message :RequestFA, fa_data_type: 3 # alias ib.wait_for :ReceiveFA end @@ -18,11 +18,11 @@ after(:all) { close_connection } subject { IB::Connection.current.received[:ReceiveFA].first } - + it_behaves_like 'ReceiveFA message' - it_behaves_like 'Valid Account Object' do - let( :the_account_object ){ IB::Connection.current.received[:ReceiveFA].first.accounts.first } - end + it_behaves_like 'Valid Account Object' do + let( :the_account_object ){ IB::Connection.current.received[:ReceiveFA].first.accounts.first } + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb b/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb index 3cef9ad..e6722fd 100644 --- a/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb +++ b/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb @@ -7,10 +7,10 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection ib = IB::Connection.current - - ib.send_message :RequestFA, fa_data_type: 3 # alias + + ib.send_message :RequestFA, fa_data_type: 3 # alias ib.wait_for :ReceiveFA end @@ -18,11 +18,11 @@ after(:all) { close_connection } subject { IB::Connection.current.received[:ReceiveFA].first } - + it_behaves_like 'ReceiveFA message' - it_behaves_like 'Valid Account Object' do - let( :the_account_object ){ IB::Connection.current.received[:ReceiveFA].first.accounts.first } - end + it_behaves_like 'Valid Account Object' do + let( :the_account_object ){ IB::Connection.current.received[:ReceiveFA].first.accounts.first } + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/messages/outgoing/account_data_spec.rb b/spec/ib/messages/outgoing/account_data_spec.rb deleted file mode 100644 index 074b578..0000000 --- a/spec/ib/messages/outgoing/account_data_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'main_helper' - -describe IB::Messages::Outgoing do - - context 'Newly instantiated Message' do - - subject do - IB::Messages::Outgoing::RequestAccountData.new( - :subscribe => true, - :account_code => 'DUH') - end - - it { is_expected.to be_an IB::Messages::Outgoing::RequestAccountData } - its(:message_type) { is_expected.to eq :RequestAccountData } - its(:message_id) { is_expected.to eq 6 } - its(:data) { is_expected.to eq({:subscribe=>true, :account_code=>"DUH"})} - its(:subscribe) { is_expected.to be_truthy } - its(:account_code) { is_expected.to eq 'DUH' } - its(:to_human) { is_expected.to match /RequestAccountData/ } - - it 'has class accessors as well' do - expect( subject.class.message_type).to eq :RequestAccountData - expect( subject.class.message_id).to eq 6 - expect( subject.class.version).to eq 2 - end - - it 'encodes into Array' do - expect( subject.encode).to eq [[6, 2], [], [true, "DUH"]] - end - - it 'that is flattened before sending it over socket to IB server' do - expect( subject.preprocess).to eq [6, 2, 1, "DUH"] - end - - it 'and has correct #to_s representation' do - expect(subject.to_s).to eq "6-2-1-DUH" - end - - end -end # describe IB::Messages:Outgoing diff --git a/spec/ib/messages/outgoing/market_data_spec.rb b/spec/ib/messages/outgoing/market_data_spec.rb new file mode 100644 index 0000000..f1e56b8 --- /dev/null +++ b/spec/ib/messages/outgoing/market_data_spec.rb @@ -0,0 +1,68 @@ +require 'main_helper' + +describe IB::Messages::Outgoing do + before( :all ) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin :symbols + end + + let( :siemens ){ IB::Stock.new symbol: 'MSFT', currency: 'USD', exchange: 'SMART' } + context 'RequestMarketData for a US-Stock' do + + subject do + IB::Messages::Outgoing::RequestMarketData.new( :contract => siemens, + :snapshot => true, + :id => 3884 ) + end + + it { is_expected.to be_an IB::Messages::Outgoing::RequestMarketData } + its(:message_type) { is_expected.to eq :RequestMarketData } + its(:message_id) { is_expected.to eq 1 } + its(:data) { is_expected.to eq({:snapshot=>true, :contract=> siemens, :id => 3884} )} + its(:to_human) { is_expected.to match /RequestMarketData/ } + + it 'has class accessors as well' do + expect( subject.class.message_type).to eq :RequestMarketData + expect( subject.class.message_id).to eq 1 + expect( subject.class.version).to eq 11 + end + + it 'encodes into an Array' do + puts "RAW" + puts subject.encode.then{|y| "[#{y}]" } + expect( subject.encode[0]).to eq [1, 11] # messageID, Version + expect( subject.encode[2][0]).to be_a Numeric # request id + expect( subject.encode[2][1]).to eq siemens.serialize_short # serialized contract + expect( subject.encode[2][2]).to be_empty # no legs + expect( subject.encode[2][3]).to be_falsy # no delta neutral contract + expect( subject.encode[2][4]).to be_empty # no Tick list + expect( subject.encode[2][5]).to be_truthy # snapshot + expect( subject.encode[2][6]).to be_falsy # regulatory snapshot + expect( subject.encode[2][7]).to be_empty # options + + end +# + it 'that is flattened before sending it over socket to IB server' do + expect( subject.preprocess).to eq [1, 11, 3884, "", "MSFT", "STK", "", "", "", "", "SMART", "", "USD", "", "", 0,"", 1, 0, ""] + end + + it 'and has a correct #to_s representation' do + expect(subject.to_s).to eq "1-11-3884--MSFT-STK-----SMART--USD---0--1-0-" + end + + end + context 'RequestMarketData for a Stock Spread' do + + subject do + IB::Messages::Outgoing::RequestMarketData.new( :contract => IB::Symbols::Combo.ib_mcd, + :snapshot => true, + :id => 3884 ) + end + + it 'encodes into an Array' do + puts "RAW" + puts subject.encode.then{|y| "[#{y}]" } + end + end +end # describe IB::Messages:Outgoing diff --git a/spec/ib/messages/outgoing/place_order_spec.rb b/spec/ib/messages/outgoing/place_order_spec.rb new file mode 100644 index 0000000..2baa73c --- /dev/null +++ b/spec/ib/messages/outgoing/place_order_spec.rb @@ -0,0 +1,49 @@ +require 'main_helper' + +RSpec.describe IB::Messages::Outgoing do + + + context 'Newly instantiated Message' do + + subject do + IB::Messages::Outgoing::PlaceOrder.new( + local_id: 123, + contract: IB::Stock.new( symbol: 'F' ), + order: IB::Order.new( total_quantity: 100, limit_price: 25, tif: :good_til_canceled )) + end + + it { should be_an IB::Messages::Outgoing::PlaceOrder } + its(:message_type) { is_expected.to eq :PlaceOrder } + its(:message_id) { is_expected.to eq 3 } +# its(:local_id) { is_expected.to eq 123 } + + it 'has class accessors as well' do + expect( subject.class.message_type).to eq :PlaceOrder + expect( subject.class.message_id).to eq 3 + expect( subject.class.version).to be_zero + end + + + it 'encodes correctly' do + expect( subject.encode[0]). to eq [3, 123, []] # msg-id, local_id + expect( subject.encode[1]). to eq ['', 'F','STK','','','','','SMART','','USD','','', "",""] # contract + expect( subject.encode[2] ).to eq [ nil, 100, "LMT",25,"" ] # basic order fields + expect( subject.encode[3] ).to eq [ "DAY", nil, nil,"O",0,nil,true, 0, false, false, nil, 0, false, false ] # extended order fields + expect( subject.encode[4]). to eq [] # empty legs + if subject.server_version < 177 + expect( subject.encode[5]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields + else + expect( subject.encode[5]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields + end + expect( subject.encode[6 .. 12]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields + expect( subject.encode[ 13 .. 22]). to eq [false, "", "", false, false, "", 0, [ nil, "", "", "", ""], false, ["", ""]] # algo order fields -1- + expect( subject.encode[23]). to eq ["",""] # empty delta neutral order fields + expect( subject.encode[24 .. 25]). to eq [0,""] + expect(subject.encode[26 .. -1]). to eq [ "", "", ["", "", "", "", "", ""], nil, [], false, nil, nil, false, [false], [""], "", false, "", false, [false, false], [], [0], ["", nil, nil, nil, nil, nil, nil], "", [nil, nil], nil, [[nil, nil], [nil, nil]], nil, nil, nil, "", nil, nil, nil, []] + +# debug puts subject.encode[24 .. -1 ].then{|y| "\n[ #{y} ]"} + end + + + end +end # describe IB::Messages:Outgoing diff --git a/spec/ib/orders/account_spec.rb b/spec/ib/orders/account_spec.rb new file mode 100644 index 0000000..c4e1370 --- /dev/null +++ b/spec/ib/orders/account_spec.rb @@ -0,0 +1,67 @@ +require 'order_helper' + +describe 'Order placement via Account' do # :connected => true, :integration => true do + let(:contract_type) { :stock } + + before(:all) do + establish_connection 'gateway' + IB::Connection.current.activate_plugin :order_prototypes, :market_price, :auto_adjust + end + + after(:all) do + remove_open_orders + clean_connection + end + + + let( :jardine ){ IB::Stock.new symbol: 'J36', exchange: 'SGX' } # trading hours: 2 - 10 am GMT, min-lot-size: 100 + let( :ge ){ IB::Stock.new symbol: 'GE', exchange: 'SMART' } + let( :tui ){IB::Stock.new symbol: :tui1, exchange: :smart, currency: :eur } + let( :the_client ){ IB::Connection.current.clients.detect{|y| y.account == ACCOUNT} } + + context 'Placing orders' do + before(:each) do + ib = IB::Connection.current + ib.clear_received # just in case ... + end + # note: if the tests don't pass, cancel all orders maually and run again (/examples/canccel_orders) + # note: We explicitly set auto-adjust to false! + it "wrong order" do + the_order= IB::Limit.order action: :buy, size: 100, :limit_price => 0.453 # non-acceptable price + expect( the_client ).to be_a IB::Account + expect{ the_client.place contract: jardine, order: the_order, auto_adjust: false } + .to raise_error( IB::SymbolError, /The price does not conform to the minimum price variation/ ) + expect( should_log /The price does not conform to the minimum price variation/ ).to be_truthy + end + it "order too small" do + the_order= IB::Limit.order action: :buy, size: 10, :limit_price => 20 # acceptable price + expect{ the_client.place contract: jardine, order: the_order } + .to raise_error( IB::SymbolError, /Order size 10 is smaller than the minimum required size of 100/) + expect( should_log /Order size 10 is smaller than the minimum required size of 100/ ).to be_truthy + end + + it "placing 10% below market price" do + puts "fetching market price – that might be slow" + mp = tui.market_price + mp = 6.to_d if mp.to_i.zero? # default-price + the_price = mp -(mp*0.1) + puts "the_price: #{the_price.to_s} ( #{the_price.class} )" + the_order= IB::Limit.order action: :buy, size: 100, :limit_price => the_price + local_id = the_client.place contract: tui, + order: the_order, + convert_size: true, + auto_adjust: true + + expect( local_id ).not_to be_nil + expect( the_client.orders ).to have_at_least(1).entry + expect( the_client.orders.first.order_states ).to have_at_least(1).entry +# puts the_client.orders.first.order_states.last.inspect + expect( the_client.orders.first.order_states.last.status).to eq( 'New') + .or eq("Submitted") + .or eq("PreSubmitted") + expect( the_client.orders.first.order_states.last.filled).to be_zero + end + end + + +end # describe diff --git a/spec/ib/orders/combo_spec.rb b/spec/ib/orders/combo_spec.rb new file mode 100644 index 0000000..d5f605a --- /dev/null +++ b/spec/ib/orders/combo_spec.rb @@ -0,0 +1,69 @@ +require 'order_helper' +require 'combo_helper' + +RSpec.describe "What IF Order" do + + + before(:all) do + establish_connection :gateway + ib = IB::Connection.current + ib.activate_plugin :symbols, :order_prototypes + end + + after(:all) { remove_open_orders; close_connection } + + context "Butterfly" do + before(:all) do + ib = IB::Connection.current + @initial_order_id = ib.next_local_id + + ib.clear_received # just in case ... + + the_contract = IB::Symbols::Combo.stoxx_straddle + market_price = 13 # the_contract.market_price + the_client = ib.clients.detect{|x| x.account == ACCOUNT } + + @local_id_placed = the_client.preview contract: the_contract, + order: IB::Limit.order( action: :buy, + limit_price: market_price , + size: 10 ) + end + + context IB::Connection , focus: true do + subject{ IB::Connection.current } + its( :next_local_id ){ is_expected.to eq @initial_order_id +1 } + it { expect( subject.received[:OpenOrder]).to have_at_least(1).open_order_message } + it { expect( subject.received[:OrderStatus]).to have_at_least(0).status_message } + it { expect( subject.received[:OrderStatus]).to be_empty } + it { expect( subject.received[:ExecutionData]).to be_empty } + it { expect( subject.received[:CommissionReport]).to be_empty } + + end + + + context IB::Messages::Incoming::OpenOrder do + subject{ IB::Connection.current.received[:OpenOrder].last } + it_behaves_like 'OpenOrder message' + end + + context IB::Order do + subject{ IB::Connection.current.received[:OpenOrder].last.order } + it_behaves_like 'Placed Order' + it_behaves_like 'Presubmitted what-if Order', IB::Bag.new + end + + ## separated from context IB::Order + #. ib.clear_received is evaluated before shared_examples are run, thus + # makes it impossible to load the order from the received-hash.. + context "finalize" do + it 'is not actually being placed though' do + ib = IB::Connection.current + ib.clear_received + ib.send_message :RequestOpenOrders + ib.wait_for :OpenOrderEnd + expect( ib.received[:OpenOrder]).to have_exactly(0).order_message + end + end # context "What if order" + end +end +__END__ diff --git a/spec/ib/orders/order_flow_spec.rb b/spec/ib/orders/order_flow_spec.rb new file mode 100644 index 0000000..f415342 --- /dev/null +++ b/spec/ib/orders/order_flow_spec.rb @@ -0,0 +1,111 @@ +require 'order_helper' + +# *notice* +# The tests below pass only if no open or pending order is present + + +describe "Order-Flow", :connected => true, :integration => true, :slow => true do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin :symbols, :order_prototypes, :order_flow + end + + context "Trading Ford", :if => :forex_trading_hours, focus: true do + + before(:all) do + ib = IB::Connection.current + @initial_order_id = ib.next_local_id + end + + after(:all) { remove_open_orders; close_connection } + + let(:contract) { IB::Symbols::Stocks.sie } + [ :buy, :sell ].each_with_index do | the_action, count | + context "Placing #{the_action.to_s.upcase} order" do + + + it " place the order" do + order = IB::Market.order( size: 2, + action: the_action, + account: ACCOUNT, + contract: contract ) + open_order = order.place + expect( open_order).to be_a IB::Order + puts open_order &.to_human + end # it + end # each + end # context + end # context +# context IB::Connection do +# subject{ IB::Connection.current } +# it { expect( subject.received[:OpenOrder]).to have_at_least(1).open_order_message } +# it { expect( subject.received[:OrderStatus]).to have_at_least(1).status_message } +# it { expect( subject.received[:ExecutionData]).to have_exactly(1).execution_data } +# it { expect( subject.received[:CommissionReport]).to have_exactly(1).report } +# +# end +# +# +# +# context IB::Messages::Incoming::OpenOrder do +# subject{ IB::Connection.current.received[:OpenOrder].first } +# it_behaves_like 'OpenOrder message' +# end +# +# context IB::Order do +# subject{ IB::Connection.current.received[:OpenOrder].last.order } +## it{ is_expected.to eql @order } uncomment to display the difference between the +# # order send to the tws and the response after filling +# it_behaves_like 'Placed Order' +# it_behaves_like 'Filled Order' +# end +# +# context IB::Messages::Incoming::ExecutionData do +# subject { IB::Connection.current.received[:ExecutionData].last } +# it_behaves_like 'Proper Execution Record' , the_action +# end +# +# context IB::Messages::Incoming::CommissionReport do +# subject{ IB::Connection.current.received[:CommissionReport].last } +# it_behaves_like 'Valid CommissionReport' , count +# end +# +# end # Placing +# end # each +# +# context "Request executions" do +# +# before(:all) do +# ib = IB::Connection.current +# @request_id = ib.send_message :RequestExecutions, +# :client_id => OPTS[:connection][:client_id], +# account: OPTS[:connection][:account], +# :time => (Time.now.utc-10).to_ib # Time zone problems possible +# ib.wait_for :ExecutionData, 3 # sec +# end +# +# after(:all) { clean_connection } +# +# it 'does not receive Order-related messages' do +# puts "time" +# puts (Time.now.utc-10).to_ib +# expect( IB::Connection.current.received[:OpenOrder]).to be_empty +# expect( IB::Connection.current.received[:OrderStatus]).to be_empty +# end +# +# it 'receives ExecutionData messages' do +# expect( IB::Connection.current.received[:ExecutionData]).to have_at_least(1).execution_data +# end +# +# +# it 'also receives Commission Reports' do +# expect( IB::Connection.current.received[:CommissionReport]).to have_at_least(2).reports +# end +# + +end # Trades + +__END__ + diff --git a/spec/ib/orders/placement_spec.rb b/spec/ib/orders/placement_spec.rb new file mode 100644 index 0000000..3267e90 --- /dev/null +++ b/spec/ib/orders/placement_spec.rb @@ -0,0 +1,175 @@ +require 'order_helper' + +describe 'Order placement' do # :connected => true, :integration => true do + let(:contract_type) { :stock } + + before(:all) do + establish_connection :gateway + ib = IB::Connection.current + ib.received = true + ib.activate_plugin :symbols, :order_flow, :order_prototypes + end + + after(:all) do + # remove_open_orders + close_connection + end + + context 'Placing wrong order', :slow => true, focus: true do + + before(:all) do + ib = IB::Connection.current + @initial_local_id = ib.next_local_id + place_the_order do | price | + IB::Limit.order :action => :buy, :size => 100, :account => ACCOUNT, + :limit_price => price * 2.001 # non-acceptable price + end + puts ib.received.keys + puts ib.received[:Alert].map &:to_human + puts ib.received[:OpenOrder] + end + + context IB::Connection do + + subject { IB::Connection.current } + + it 'does not place new Order' do + expect( subject.received[:OpenOrder] ).to be_empty + expect( subject.received[:OrderStatus] ).to be_empty + end + + it 'still changes client`s next_local_id' do + expect( subject.current.next_local_id ).to eq @initial_local_id +1 + end + + it_has_message "Alert message" , /The price does not conform to the minimum price variation for this contract/ + + + end # context IB::Connection + + end # Placing wrong order + + context 'What-if order' do + before(:all) do + ib = IB::Connection.current + + @initial_local_id = ib.next_local_id + @local_id = place_the_order do | market_price | + IB::Limit.order action: :buy, size: 100, + :limit_price => market_price - 1, # Set acceptable price + :what_if => true, # Hypothetical + account: ACCOUNT + end + end + + it 'changes client`s next_local_id' do + expect( IB::Connection.current.next_local_id ).to eq @initial_local_id +1 + end + + it { expect( IB::Connection.current.received[:OpenOrder]).to have_at_least(1).open_order_message } + it { expect( IB::Connection.current.received[:OrderStatus]).to have_exactly(0).status_messages } + context IB::Order do + subject { IB::Connection.current.received[:OpenOrder].last.order } + it_behaves_like 'Placed Order' + it_behaves_like 'Presubmitted what-if Order' + end + + context "finalize" do + it 'is not actually being placed though' do + ib = IB::Connection.current + ib.clear_received + ib.send_message :RequestOpenOrders + ib.wait_for :OpenOrderEnd + expect( ib.received[:OpenOrder] ).to have_exactly(0).order_message + end + end + end + + context 'Off-market limit' do + before(:all) do + ib = IB::Connection.current + @initial_local_id = ib.next_local_id + place_the_order do | market_price | + IB::Limit.order action: :buy, size: 100, :limit_price => market_price-1, # Acceptable price + account: ACCOUNT + end + end + + context IB::Order do + subject { IB::Connection.current.received[:OpenOrder].last.order } + it_behaves_like 'Placed Order' + end + + context "Cancelling wrong order" do + before(:all) do + ib = IB::Connection.current + ib.clear_received + @initial_local_id = ib.next_local_id + ib.cancel_order rand( 99999999 ) + + ib.wait_for :Alert + end + + it { puts IB::Connection.current.received[:Alert].to_human } + it { expect( IB::Connection.current.received[:Alert]).to have_at_least( 1 ).alert_message } + + it 'does not increase client`s next_local_id further' do + expect( IB::Connection.current.next_local_id ).to eq @initial_local_id + end + + it 'does not receive Order messages' do + ib = IB::Connection.current + puts ib.received[:OrderStatus].to_human + expect( ib.received?(:OrderStatus)).to be_falsy + expect( ib.received?(:OpenOrder)).to be_falsy + end + it_has_message "Alert message" , /OrderId \d* that needs to be cancelled is not found/ + + end + end # Off-market limit + + context 'order with conditions' do + before(:all) do + ib = IB::Connection.current + + @initial_local_id = ib.next_local_id + @local_id = place_the_order do | market_price | + + condition1 = IB::MarginCondition.new percent: 45, operator: '<=' + condition2 = IB::PriceCondition.fabricate IB::Symbols::Futures.es, "<=", 2600 + + IB::Limit.order action: :buy, size: 100, + conditions: [condition1, condition2], + conditions_cancel_order: true , + :limit_price => market_price - 1, # Set acceptable price + account: ACCOUNT + end + end + + it 'changes client`s next_local_id' do + expect( IB::Connection.current.next_local_id ).to eq @initial_local_id + 1 + end + + it { expect( IB::Connection.current.received[:OpenOrder]).to have_at_least( 1 ).open_order_message } +# it "display order_stauts" do +# puts IB::Connection.current.received[:OrderStatus].inspect +# end + it { expect( IB::Connection.current.received[:OrderStatus]).to have_exactly( 1 ).status_messages } + context IB::Order do + subject { IB::Connection.current.received[:OpenOrder].last.order } + it_behaves_like 'Placed Order' + + it "contains proper conditions" do + expect( subject.conditions ).to be_an Array + expect( subject.conditions ).to have( 2 ).conditions + expect( subject.conditions.first ).to be_an IB::MarginCondition + expect( subject.conditions.last ).to be_an IB::PriceCondition + expect( subject.conditions_cancel_order ).to be_truthy + end + end + + end + + + context '' +end # Orders diff --git a/spec/ib/orders/trades_spec.rb b/spec/ib/orders/trades_spec.rb new file mode 100644 index 0000000..e96deb8 --- /dev/null +++ b/spec/ib/orders/trades_spec.rb @@ -0,0 +1,122 @@ +require 'order_helper' + +# *notice* +# The tests below pass only if no open or pending order is present + + +describe "Trades", :connected => true, :integration => true, :slow => true do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin :symbols, :order_prototypes + end + + context "Trading Forex", :if => :forex_trading_hours, focus: true do + + before(:all) do + ib = IB::Connection.current + @initial_order_id = ib.next_local_id + end + + after(:all) { remove_open_orders; close_connection } + + let(:contract) { IB::Symbols::Forex[:eurusd] } # referenced by shared examples + [ :buy, :sell ].each_with_index do | the_action, count | + context "Placing #{the_action.to_s.upcase} order" do + + let(:order) { IB::Market.order( size: 20000, action: the_action, account: ACCOUNT ) } # referenced by shared examples + before(:all) do + ib = IB::Connection.current + ib.clear_received + open_order = place_the_order contract: IB::Symbols::Forex.eurusd do | price | + IB::Market.order :size => 20000, # order and contract cannot be used on this level + :action => the_action, + :account => ACCOUNT + end + @initial_order_id = open_order.local_id + ib.wait_for :ExecutionData, 5 + + puts "open Order" + puts open_order.to_human + ib.wait_for :Execution + end + + after(:all) do + clean_connection # Clear logs and message collector + # IB::Connection.current.cancel_order @local_id_placed # Just in case... + end + + context IB::Connection do + subject{ IB::Connection.current } + it { expect( subject.received[:OpenOrder]).to have_at_least(1).open_order_message } + it { expect( subject.received[:OrderStatus]).to have_at_least(1).status_message } + it { expect( subject.received[:ExecutionData]).to have_exactly(1).execution_data } + it { expect( subject.received[:CommissionReport]).to have_exactly(1).report } + + end + + + + context IB::Messages::Incoming::OpenOrder do + subject{ IB::Connection.current.received[:OpenOrder].first } + it_behaves_like 'OpenOrder message' + end + + context IB::Order do + subject{ IB::Connection.current.received[:OpenOrder].last.order } +# it{ is_expected.to eql @order } uncomment to display the difference between the + # order send to the tws and the response after filling + it_behaves_like 'Placed Order' + it_behaves_like 'Filled Order' + end + + context IB::Messages::Incoming::ExecutionData do + subject { IB::Connection.current.received[:ExecutionData].last } + it_behaves_like 'Proper Execution Record' , the_action + end + + context IB::Messages::Incoming::CommissionReport do + subject{ IB::Connection.current.received[:CommissionReport].last } + it_behaves_like 'Valid CommissionReport' , count + end + + end # Placing + end # each + + context "Request executions" do + + before(:all) do + ib = IB::Connection.current + @request_id = ib.send_message :RequestExecutions, + :client_id => OPTS[:connection][:client_id], + account: OPTS[:connection][:account], + :time => (Time.now.utc-10).to_ib # Time zone problems possible + ib.wait_for :ExecutionData, 3 # sec + end + + after(:all) { clean_connection } + + it 'does not receive Order-related messages' do + puts "time" + puts (Time.now.utc-10).to_ib + expect( IB::Connection.current.received[:OpenOrder]).to be_empty + expect( IB::Connection.current.received[:OrderStatus]).to be_empty + end + + it 'receives ExecutionData messages' do + expect( IB::Connection.current.received[:ExecutionData]).to have_at_least(1).execution_data + end + + + it 'also receives Commission Reports' do + expect( IB::Connection.current.received[:CommissionReport]).to have_at_least(2).reports + end + + end # Request executions + end # Forex order + +end # Trades + +__END__ + diff --git a/spec/ib/plugins/auto_adjust_spec.rb b/spec/ib/plugins/auto_adjust_spec.rb new file mode 100644 index 0000000..8ef995f --- /dev/null +++ b/spec/ib/plugins/auto_adjust_spec.rb @@ -0,0 +1,61 @@ +require "main_helper" + +describe "Connect to TWS and activate Plugin" do + before(:all) do + establish_connection :gateway + c = IB::Connection.current + c.activate_plugin "verify" + c.activate_plugin "order-prototypes" + c.activate_plugin "auto-adjust" + end + + after(:all) { close_connection } + + context "A new connection is established" do + it{ expect( IB::Connection.current ).to be_a IB::Connection } + end + + context "Read min_tick" do + Given( :m_stock ) { IB::Stock.new( symbol: 'M' ).verify.first } + When( :min_tick ){ m_stock.contract_detail.min_tick } + Then { min_tick == 0.01 } + end + + context "Working on an ordenary us-stock (2 digits) [contract is not verified]" do + Given( :stock ) { IB::Stock.new( symbol: 'M' ) } + context "Create an order with a suitable price" do + Given( :order ) { IB::Limit.order price: 50.01, size: 100, action: :buy, contract: stock } + When { order.auto_adjust } + Then { order.limit_price == 50.01 } + end + context "Create an order which needs to be auto adjusted" do + Given( :order1 ) { IB::Limit.order price: 50.024, size: 100, action: :buy, contract: stock } + When { order1.auto_adjust } + Then { order1.limit_price == 50.02 } + Given( :order2 ) { IB::Limit.order price: 50.026, size: 100, action: :buy, contract: stock } + When { order2.auto_adjust } + Then { order2.limit_price == 50.03 } + end + end + context "Working on an european-stock with 4 digits [contract is verified]" do + Given( :base_stock ) { IB::Stock.new( symbol: 'TKA', currency: :eur ) } + When( :stock ) { base_stock.verify.first } + When( :min_tick ){ stock.contract_detail.min_tick } + Then { min_tick == 0.0001 } + context "Create an order with a suitable price" do + Given( :order ) { IB::Limit.order price: 5.001, size: 100, action: :buy, contract: stock } + When { order.auto_adjust } + Then { order.limit_price == 5.001 } + end + context "Create an order which needs to be auto adjusted" do + Given( :order1 ) { IB::Limit.order price: 5.0024, size: 100, action: :buy, contract: stock } + When { order1.auto_adjust } + Then { order1.limit_price == 5.002 } + Given( :order2 ) { IB::Limit.order price: 5.0026, size: 100, action: :buy, contract: stock } + When { order2.auto_adjust } + Then { order2.limit_price == 5.003 } + end + end +end + + diff --git a/spec/ib/plugins/managed_account_spec.rb b/spec/ib/plugins/managed_account_spec.rb new file mode 100644 index 0000000..f2e61dc --- /dev/null +++ b/spec/ib/plugins/managed_account_spec.rb @@ -0,0 +1,36 @@ +require "main_helper" + +describe "Connect to Gateway or TWS" do + before(:all){ establish_connection 'managed-accounts'} + + after(:all) { close_connection } + + + context "Active Connection" do + Given( :current ){ IB::Connection.current } + Then { current.is_a? IB::Connection } + Then { current.plugins.include? 'managed-accounts' } + + context "Plugin works as expected" do + Given( :clients ){ current.clients } + Then { clients.is_a? Array } + Then { clients.size >= 1 } + Given( :advisor ){ current.advisor } + Then { advisor.is_a? IB::Account } + Then { advisor.account =~ /F/ } + Given( :client ){ clients.first } + Then { client.is_a? IB::Account } + Then { client.account =~ /U/ } + Then { client.portfolio_values.is_a? Array } + Then { client.contracts.is_a? Array } + Then { client.account_values.is_a? Array } + When( :all_contracts ){ client.contracts } + Then { all_contracts.map{ |c| c.is_a? IB::Contract }.uniq == [true] } + When( :all_portfolio_positions ){ client.portfolio_values } + Then { all_portfolio_positions.map{ |p| p.is_a? IB::PortfolioValue }.uniq == [true] } + + end + end +end + + diff --git a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb new file mode 100644 index 0000000..a6bc6ac --- /dev/null +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -0,0 +1,37 @@ +require 'main_helper' +require 'order_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + context 'Adaptive Limit Order ' do + + Given( :soft ){ IB::Stock.new symbol: 'MSFT' } + Given( :size ){ 100 } + Given( :price){ 200 } + When( :order ){ IB::Adaptive.order size: size, price: price, contract: soft, account: ACCOUNT } + it { puts order.as_table } + context "Main Order Fields show a Limit Order" do + Then { order.serialize_main_order_fields == [ "BUY", size, "LMT", price, ""] } + end + context "Limit Orders are submitted as GTC" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Algo specific fields are serialized" do + Then { order.serialize_algo == [ "Adaptive", 1, ["adaptivePriority", "Normal"] ] } + end + context "Order specifies as Limit" do + subject{ order } + it_behaves_like "serialize limit order fields" + end + + end + +end # describe IB::Messages:Outgoing diff --git a/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb new file mode 100644 index 0000000..30354eb --- /dev/null +++ b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb @@ -0,0 +1,62 @@ +require 'main_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection 'gateway' + ib = IB::Connection.current + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + ib.activate_plugin 'process-orders' + ib.activate_plugin 'order-flow' + + end + + context 'Discretionary Order Prototype' do + + Given( :volatile_stock ){ IB::Stock.new symbol: 'TSLA' } + Given( :size ){ 100 } + Given( :price){ 380 } # Public Limit price + Given( :secret ){ 5 } # Secret discount offered to the seller + When( :order ){ IB::Discretionary.order size: size, + price: price, + dc: secret, + contract: volatile_stock, + account: ACCOUNT } + it { puts order.as_table } + context "Main Order Fields show a Limit Order" do + Then { order.serialize_main_order_fields == [ "BUY", size, "LMT", price, ""] } + end + context "Limit Orders are submitted as GTC" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "The disretionary amount order field is set" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", secret ] } + end + context "Other Fields are zero or empty" do + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + + context "place example orders" do + it "place tesla" do + client= IB::Connection.current.clients.detect{| i | i.account == ACCOUNT } + tesla_order = IB::Discretionary.order size: 100, price: price, dc: secret + ## using preview to pretect from unwanted executions + client.preview order: tesla_order, contract: volatile_stock + expect( client.orders.size ).to be > 0 + expect( client.orders.last.contract).to eq volatile_stock + end + + + end + + end + +end # describe IB::Messages:Outgoing diff --git a/spec/ib/plugins/order-prototypes/limit_order_spec.rb b/spec/ib/plugins/order-prototypes/limit_order_spec.rb new file mode 100644 index 0000000..02333cf --- /dev/null +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -0,0 +1,41 @@ +require 'main_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + context 'Limit Order Prototype' do + + Given( :soft ){ IB::Stock.new symbol: 'MSFT' } + Given( :size ){ 100 } + Given( :price){ 200 } + When( :order ){ IB::Limit.order size: size, price: price, contract: soft, account: ACCOUNT } + it { puts order.as_table } + context "Main Order Fields show a Limit Order" do + Then { order.serialize_main_order_fields == [ "BUY", size, "LMT", price, ""] } + end + context "Limit Orders are submitted as GTC" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Other Fields are zero or empty" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + + end + +# +end # describe IB::Messages:Outgoing diff --git a/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb new file mode 100644 index 0000000..a9c6858 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb @@ -0,0 +1,53 @@ +require 'main_helper' + +RSpec.describe IB::Connection do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + ib.activate_plugin 'market-price' + + end + + + context 'Pegged Bench Order ft as Benchmark' do + + Given( :stock ){ IB::Stock.new( symbol: 'IBM' ) } + Given( :benchmark ){ IB::Symbols::Stocks.msft_conid } # benchmark is always identified by its conid + Given( :increment ){ 1 } + Given( :reference_increment ){ 2 } + + When( :order ){ IB::Pegged2Benchmark.order size: 100, starting_price: 450, change_by: increment, + reference: benchmark.con_id, + reference_change_by: reference_increment, + contract: stock, account: ACCOUNT } + it{ puts order.as_table } + context "Main Order Fields show a Pegged to Benchmark order" do + Then { order.serialize_main_order_fields == [ "BUY", 100, "PEG BENCH", "", "" ] } + end + context "Pegged orders are submitted as daily orders" do + Then { order.serialize_extended_order_fields == ["DAY", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Pegged order fields are populated" do + Then { order.serialize_pegged_order_fields == [ benchmark.con_id, + false, # increase + increment, + reference_increment, + '' ] } # reference exchange + end + context "Other order fields are zero or empty" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + + end +end diff --git a/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb new file mode 100644 index 0000000..87096a0 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb @@ -0,0 +1,41 @@ +require 'main_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + STRIKE = 2000 + + context 'Pegged to Primary Order Prototype with a price cap' do + + Given( :stock ){ IB::Stock.new symbol: "MSFT"} + + When( :order ){ IB::Pegged2Primary.order size: 100, price_cap: 450, offset_amount: 0.1, + contract: stock, account: ACCOUNT } + it{ puts order.as_table } + context "Main Order Fields show a REL order" do + Then { order.serialize_main_order_fields == [ "BUY", 100, "REL", 450, 0.1 ] } + end + context "Pegged orders are submitted as daily orders" do + Then { order.serialize_extended_order_fields == ["DAY", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Normal order nns apply" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + + end +end diff --git a/spec/ib/plugins/order-prototypes/pegged2stock_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2stock_order_spec.rb new file mode 100644 index 0000000..40de453 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/pegged2stock_order_spec.rb @@ -0,0 +1,51 @@ +require 'main_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + STRIKE = 2000 + + context 'Pegged to Stock order to dynamically sell a put option on IBM' do + + Given( :option ){ IB::Symbols::Options.ibm.merge( strike: 150 )} + Given( :starting_price ){ 2 } + Given( :delta ){ 0.2 } + Given( :size ){ -1 } + + When( :order ){ IB::Pegged2Stock.order size: size, delta: delta, starting_price: starting_price, + contract: option, account: ACCOUNT } + it{ puts order.as_table } + context "Main Order Fields show a PEG STK order" do + Then { order.serialize_main_order_fields == [ "SELL", 1, "PEG STK", "", "" ] } + end + context "Pegged orders are submitted as daily orders" do + Then { order.serialize_extended_order_fields == ["DAY", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Advanced option creteria apply" do + Then { order.serialize_advanced_option_order_fields == [ starting_price, "", delta, "", "" ] } + + end + context "Other order fields are zero or empty" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + +# it{ puts IB::Messages::Outgoing::PlaceOrder.new( local_id: 2, contract: option, order: order ).to_s } + end + + end +end diff --git a/spec/ib/plugins/order-prototypes/stop_order_spec.rb b/spec/ib/plugins/order-prototypes/stop_order_spec.rb new file mode 100644 index 0000000..362a65b --- /dev/null +++ b/spec/ib/plugins/order-prototypes/stop_order_spec.rb @@ -0,0 +1,40 @@ +require 'main_helper' + +RSpec.describe IB::Connection do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + context 'Stop Order Prototype' do + + Given( :soft ){ IB::Stock.new symbol: 'MSFT' } + When( :order ){ IB::SimpleStop.order size: -100, price: 200, contract: soft, account: ACCOUNT } + it{ puts order.as_table } + context "Main Order Fields show a STP Order" do + Then { order.serialize_main_order_fields == [ "SELL", 100, "STP", "",200 ] } + end + context "Stop Orders are submitted as GTC" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + + context "Other Order fields are zero or empty" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_volatility_order_fields.uniq == [ "" ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + end + +# +# +end # describe IB::Messages:Outgoing diff --git a/spec/ib/plugins/order-prototypes/volatility_order_spec.rb b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb new file mode 100644 index 0000000..9eaae22 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb @@ -0,0 +1,42 @@ +require 'main_helper' + +RSpec.describe IB::Order do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + context 'Volatility Order Prototype' do + + Given( :strike ){ 2000 } + Given( :option ){ IB::Symbols::Options.rutw.merge( strike: strike, expiry: IB::Future.next_expiry[0..-3] ).verify.first } + + When( :order ){ IB::Volatility.order size: -1, volatility: 0.2, contract: option, account: ACCOUNT } + it{ puts order.as_table } + context "Main Order Fields show a VOL Order" do + Then { order.serialize_main_order_fields == [ "SELL", 1, "VOL", "", "" ] } + end + context "Volatility Orders are submitted as daily orders" do + Then { order.serialize_extended_order_fields == ["DAY", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + context "Volatility specific orderfields are populated; volatility is expessed annualy" do + Then { order.serialize_volatility_order_fields == [ 0.2, 2] } + end + context "Other order fields are zero or empty" do + Then { order.serialize_auxilery_order_fields.flatten.compact == [ "", 0 ] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields.uniq == [""] } + Then { order.serialize_delta_neutral_order_fields.uniq == [ "" ] } + Then { order.serialize_pegged_order_fields.empty? } + Then { order.serialize_mifid_order_fields.flatten.compact.empty? } + Then { order.serialize_peg_best_and_mid.empty? } + end + + end +end diff --git a/spec/ib/plugins/verify_spec.rb b/spec/ib/plugins/verify_spec.rb new file mode 100644 index 0000000..e9ef23f --- /dev/null +++ b/spec/ib/plugins/verify_spec.rb @@ -0,0 +1,39 @@ +require "main_helper" + +describe "Connect to Gateway or TWS" do + before(:all){ establish_connection } + + after(:all) { close_connection } + + context "A new connection" do + it{ expect( IB::Connection.current ).to be_a IB::Connection } + end + + # context "Plugin not present" do + # Given( :current ){ IB::Connection.current } + # Then { current.plugins == [] } + # it { expect{ current.activate_plugin('invalid') }.to raise_error IB::Error } + # end + + context "Verify Plugin" do + let( :stock ) { IB::Stock.new symbol: 'M' } + + it "Raises NoMethodError if the verify plugin is not activated" do + expect{ stock.verify }.to raise_error NoMethodError + end + + it "Gets the ConId if the Contact after the Verify Plugin is activated" do + + current = IB::Connection.current + status = current.activate_plugin('verify') + expect( status ).to be_truthy + + verified_stocks = stock.verify + expect( verified_stocks).to be_a Array + complete_stock = verified_stocks.first + expect( complete_stock.con_id).to be > 0 + end + end +end + + diff --git a/spec/ib/raw_message_spec.rb b/spec/ib/raw_message_spec.rb new file mode 100644 index 0000000..f8ba41f --- /dev/null +++ b/spec/ib/raw_message_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require_relative '../../lib/ib/raw_message_parser' + +# recvfrom return a message in first record of +# array. +def array_msg(msg) + [msg,nil] +end + +# Each test shouldn't get to this message. +BAD_MSG = array_msg('x').freeze + +VALID_MESSAGE = ['137', '20220719 18:46:24 Central Standard Time'].freeze +MAX_ITERATIONS = 50 +RSpec.describe IB::RawMessageParser do + it 'Parser Receives a full message from the TCP Socket' do + full_message = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00") + socket = double + allow(socket).to receive(:recvfrom).and_return(full_message, BAD_MSG) + + parser = IB::RawMessageParser.new(socket) + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq (1) + end + + it 'Parser Receives a full message in two chunks from the TCP Socket' do + part_a = array_msg("\x00\x00\x00,137\x002022") + part_b = array_msg("0719 18:46:24 Central Standard Time\x00") + socket = double + allow(socket).to receive(:recvfrom).and_return(part_a, part_b, BAD_MSG) + + parser = IB::RawMessageParser.new(socket) + + #shouldn't return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(0) + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + end + + it 'Parser Receives two full messages' do + full_message_a = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00") + full_message_b = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00") + socket = double + allow(socket).to receive(:recvfrom).and_return(full_message_a, full_message_b, BAD_MSG) + + #should return a message + counter = 0 + parser = IB::RawMessageParser.new(socket) + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + + end + + it 'Parser Receives two full messages in one recvfrom call' do + message = "\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00" + two_messages = array_msg(message+message) + socket = double + allow(socket).to receive(:recvfrom).and_return(two_messages, BAD_MSG) + + #should return a message + counter = 0 + parser = IB::RawMessageParser.new(socket) + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 2 + end + expect(counter).to eq(2) + + end + + it 'Parser Receives a full message and a half, the rest in chunk b ' do + full_message = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00\x00\x00\x00,137\x002022") + part_message = array_msg("0719 18:46:24 Central Standard Time\x00") + socket = double + allow(socket).to receive(:recvfrom).and_return(full_message, part_message, BAD_MSG) + + parser = IB::RawMessageParser.new(socket) + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + end + + it 'Parser Receives a full message and the first byte of a second, the rest in chunk b ' do + full_message = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00\x00") + part_message = array_msg("\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x00") + socket = double + allow(socket).to receive(:recvfrom).and_return(full_message, part_message, BAD_MSG) + + parser = IB::RawMessageParser.new(socket) + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + + + #should return a message + counter = 0 + parser.each do |message| + expect(message).to eq(VALID_MESSAGE) + + counter += 1 + break if counter >= 1 + end + expect(counter).to eq(1) + end + + bad_message = array_msg("\x00\x00\x00,137\x0020220719 18:46:24 Central Standard Time\x01") + it 'Parser Receives an invalid message (last byte is not \\x00)' do + socket = double + allow(socket).to receive(:recvfrom).and_return(bad_message) + + parser = IB::RawMessageParser.new(socket) + expect { parser.each { |msg| } }.to raise_error(StandardError, /invalid last byte/) + end +end diff --git a/spec/ib/stock_spec.rb b/spec/ib/stock_spec.rb new file mode 100644 index 0000000..0da2456 --- /dev/null +++ b/spec/ib/stock_spec.rb @@ -0,0 +1,56 @@ +require "main_helper" + +describe IB::Stock do + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'verify' + ib.activate_plugin 'symbols' + end + + after(:all) { close_connection } + + describe "Equility of Stock Contracts" do + Given( :msft ) { IB::Symbols::Stocks.msft } + Then { msft.is_a? IB::Stock } + describe "specify the symbol as symbol" do + Given( :ms_stock ){ IB::Stock.new symbol: :msft } + Then { ms_stock.is_a? IB::Stock } + Then { ms_stock == msft } + end + + describe "specify the symbol as string " do + Given( :ms_stock ){ IB::Stock.new symbol: 'msft' } + Then { ms_stock.is_a? IB::Stock } + Then { ms_stock == msft } + end + end + + + describe "Merging of Attributes" do + + Given( :msft ) { IB::Symbols::Stocks.msft } + When( :verified_microsoft ){ msft.verify.first } + Then{ msft == verified_microsoft } + Then{ verified_microsoft.con_id == 272093 } + Then{ verified_microsoft.contract_detail.is_a? IB::ContractDetail } + + describe "merging of similar stocks is possible" do + Given( :ford ){ verified_microsoft.merge symbol: 'F' } + Then{ ford.con_id.zero? } + Then{ ford.contract_detail.nil? } + When( :verified_ford ){ ford.verify.first } + Then{ verified_ford.con_id == 9599491} + end + + describe "even merging of stocks from different countries is possible" do + Given( :siemens_energy ){ verified_microsoft.merge symbol: :enr, currency: :eur } + Then{ siemens_energy.con_id.zero? } + When( :verified_enr ){ siemens_energy.verify.first } + # Then{ expect { siemens_energy.verify }.to raise_error( IB::VerifyError ) } + Then{ verified_enr.con_id == 447545380 } + end + + end +end + diff --git a/spec/main_helper.rb b/spec/main_helper.rb index 48c9ca1..cdecf74 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -34,26 +34,44 @@ def should_not_log *patterns end -## Connection helpers -def establish_connection - - ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) - if ib - ib.wait_for :ManagedAccounts, 5 - - raise "Unable to verify IB PAPER ACCOUNT" unless ib.received?(:ManagedAccounts) - - received = ib.received[:ManagedAccounts].first.accounts_list.split(',') - unless received.include?(ACCOUNT) - close_connection - raise "Connected to wrong account #{received}, expected #{ACCOUNT}" - end - puts "Performing tests with ClientId: #{ib.client_id}" - OPTS[:account_verified] = true - else - OPTS[:account_verified] = false - raise "could not establish connection!" - end +## Connection helper +# In gateway-mode ( establish_connection :gateway ) Connection#Clients is initialized, +# open orders apperar Connection#Client#Orders +# +# otherwise anything works through the Connection#Received Hash + +def establish_connection *plugins + ib = nil + accounts = nil + if plugins.map( &:to_s ).then {|y| y.include?("managed-accounts") ||y.include?("process-orders") || y.include?('gateway')} + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) + ib.activate_plugin 'verify', 'process-orders', 'advanced-account' + ib.get_account_data + ib.request_open_orders + accounts = ib.clients.map(&:account) + puts "Accounts: #{accounts}" + + else + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) + ib.received = true + ib.try_connection! + ib.wait_for :ManagedAccounts, 5 + + raise "Unable to verify IB PAPER ACCOUNT" unless ib.received?(:ManagedAccounts) + + accounts = ib.received[:ManagedAccounts].first.accounts_list.split(',') + end + if ib + unless accounts.include?(ACCOUNT) + close_connection + raise "Connected to wrong account ! Expected #{ACCOUNT} to be included in #{accounts}, \n edit \'spec/config.yml\' " + end + puts "Performing tests with ClientId: #{ib.client_id}" + OPTS[:account_verified] = true + else + OPTS[:account_verified] = false + raise "could not establish connection!" + end end @@ -61,21 +79,18 @@ def establish_connection # Clear logs and message collector. Output may be silenced. def clean_connection - ib = IB::Connection.current - if ib - if OPTS[:verbose] - puts ib.received.map { |type, msg| [" #{type}:", msg.map(&:to_human)] } - puts " Logs:", log_entries if @stdout - end - @stdout.string = '' if @stdout - ib.clear_received - end + ib = IB::Connection.current + if ib + if OPTS[:verbose] + puts ib.received.map { |type, msg| [" #{type}:", msg.map(&:to_human)] } + puts " Logs:", log_entries if @stdout + end + @stdout.string = '' if @stdout + ib.clear_received + end end def close_connection - ib = IB::Connection.current - if ib - clean_connection - ib.close - end + clean_connection + IB::Connection.current.disconnect! unless IB::Connection.current.workflow_state == 'disconnected' end diff --git a/spec/order_helper.rb b/spec/order_helper.rb index 305f427..dafea49 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -3,62 +3,65 @@ Unified Approach placing an Order order_id = place_the_oder contract:{a valid IB::Contract} do | the_last_market:price | - { modify the price as needed } - { Provide a valid IB::Order, use the appropiate OrderPrototype } + { modify the price as needed } + { Provide a valid IB::Order, use the appropiate OrderPrototype } end if the order-object provides a local_id, the order is modified. =end -def place_the_order( contract: IB::Symbols::Stocks.wfc ) - ib = IB::Connection.current - raise 'Unable to place order, no connection' unless ib && ib.connected? - order = yield( get_contract_price( contract: contract) ) - - the_order_id = if order.local_id.present? - ib.modify_order order, contract - else - ib.place_order order, contract - end - ib.wait_for :OpenOrder, 3 - the_order_id # return value +def place_the_order( contract: IB::Symbols::Stocks.wfc ) + order = yield( get_contract_price( contract: contract) ) + connection = IB::Connection.current + + IB::Connection.current.clear_received + contract= if contract.con_id.to_i > 0 + Contract.new con_id: contract.con_id, + exchange: contract.exchange + else + contract + end + local_id = connection.place_order order, contract + IB::Connection.current.wait_for :OpenOrder, 5 + puts IB::Connection.current.received[:Alert] &.to_human + IB::Connection.current.received[:OpenOrder].find{|x| x.local_id == local_id } # return OpenOrder Record end def get_contract_price contract: IB::Symbols::Stocks.wfc - ib = IB::Connection.current - ib.send_message :RequestMarketDataType, :market_data_type => :delayed - the_id = ib.send_message :RequestMarketData, contract: contract - ib.wait_for :TickPrice - ib.send_message :CancelMarketData, id: the_id - last_price = ib.received[:TickPrice].price.map(&:to_f).max - ib.clear_received :TickPrice - last_price = last_price.nil? ? rand(999).to_f/100 : last_price # use random price for testing + ib = IB::Connection.current + ib.send_message :RequestMarketDataType, :market_data_type => :delayed + the_id = ib.send_message :RequestMarketData, contract: contract + ib.wait_for :TickPrice + ib.send_message :CancelMarketData, id: the_id + last_price = ib.received[:TickPrice].price.map(&:to_f).max + ib.clear_received :TickPrice + last_price = last_price.nil? ? rand(999).to_f/100 : last_price # use random price for testing end def remove_open_orders - ib = IB::Connection.current - ib.send_message :RequestOpenOrders - ib.wait_for :OpenOrderEnd - open_order_ids = ib.received[:OpenOrder].map{|msg| msg.order[:local_id]} - ib.cancel_order *open_order_ids + ib = IB::Connection.current + ib.send_message :RequestOpenOrders + ib.wait_for :OpenOrderEnd + open_order_ids = ib.received[:OpenOrder].map{|msg| msg.order[:local_id]} + ib.cancel_order *open_order_ids end RSpec.shared_examples_for "Alert message" do | the_expected_message | - subject { IB::Connection.current.received[:Alert] } - it { is_expected.to have_at_least(1).error_message } -# it { puts "ALERT: "+ subject.inspect } # debug - it "contains a discriptive error message" do - expect( subject.any?{|x| x.message =~ the_expected_message } ).to be_truthy - end + subject { IB::Connection.current.received[:Alert] } + it { is_expected.to have_at_least(1).error_message } +# it { puts "ALERT: "+ subject.inspect } # debug + it "contains a discriptive error message" do + expect( subject.any?{|x| x.message =~ the_expected_message } ).to be_truthy + end end shared_examples_for 'OpenOrder message' do it { should be_an IB::Messages::Incoming::OpenOrder } its(:message_type) { is_expected.to eq :OpenOrder } its(:message_id) { is_expected.to eq 5 } - its(:version) { is_expected.to eq 34} +# its(:version) { is_expected.to eq 34} its(:data) { is_expected.not_to be_empty } its(:buffer ) { is_expected.to be_empty } # Work on openOrder-Message has to be finished. - ## Integration of Conditions ! + ## Integration of Conditions ! its(:local_id) { is_expected.to be_an Integer } its(:status) { is_expected.to match /Submit/ } its(:to_human) { is_expected.to match / true do -# pending "seems to be irrelevant, but needs clarification" -# expect( subject.trail_stop_price ).to be_nil.or be_zero -# end -# end + it{ is_expected.to be_a IB::Order } + it "got proper id's" do + expect( subject.local_id ).to be_an Integer + expect( subject.perm_id ).to be_an Integer + expect( subject.perm_id.to_s).to match /^\d{8,11}$/ # has 9 to 11 numeric characters + end + it "has an adequat clearing intent" do + expect(IB::VALUES[:clearing_intent].values). to include subject.clearing_intent + end + it " the Time in Force is valid" do + expect( IB::VALUES[:tif].values ).to include subject.tif + end + its( :clearing_intent ){is_expected.to eq :ib } +# it "mysterious trailing stop price is absent", :pending => true do +# pending "seems to be irrelevant, but needs clarification" +# expect( subject.trail_stop_price ).to be_nil.or be_zero +# end +# end end RSpec.shared_examples_for 'Presubmitted what-if Order' do | used_contract | - its( :status ){ is_expected.to eq 'PreSubmitted' } - if used_contract.is_a? IB::Bag ## Combos dont have fixed commissions - its( :commission ){ is_expected.to be_nil.or be_zero } - else - its( :commission ){ is_expected.to be_a( BigDecimal ).and be > 0 } - end - - its( :what_if ){ is_expected.to be_truthy } - its( :equity_with_loan ){ is_expected.to be_a( BigDecimal ).and be > 0 } - its( :init_margin ){ is_expected.to be_a( BigDecimal ).and be > 0 } - its( :maint_margin ){ is_expected.to be_a( BigDecimal ).and be > 0 } -# it "mysterious trailing stop price is absent", pending: true do -# pending "seems to be irrelevant, but needs clarification" -# expect( subject.trail_stop_price ).to be_nil.or be_zero -# end + its( :status ){ is_expected.to eq 'PreSubmitted' } + if used_contract.is_a? IB::Bag ## Combos dont have fixed commissions + its( :commission ){ is_expected.to be_nil.or be_zero } + else + its( :commission ){ is_expected.to be_a( BigDecimal ).and be > 0 } + end + + its( :what_if ){ is_expected.to be_truthy } + its( :equity_with_loan ){ is_expected.to be_a( BigDecimal ).and be > 0 } + its( :init_margin ){ is_expected.to be_a( BigDecimal ).and be > 0 } + its( :maint_margin ){ is_expected.to be_a( BigDecimal ).and be > 0 } +# it "mysterious trailing stop price is absent", pending: true do +# pending "seems to be irrelevant, but needs clarification" +# expect( subject.trail_stop_price ).to be_nil.or be_zero +# end end RSpec.shared_examples_for 'Filled Order' do - its( :commission){ is_expected.to be_a( BigDecimal ).and be > 0 } -# its( :average_fill_price ){ is_expected.not_to be_nil.or be_zero } -# its( :average_fill_price ){is_expected.to be_a BigDecimal } - its( :status ) { is_expected.to eq 'Filled' } -# it "mysterious trailing stop price is absent", pending: true do -# pending "seems to be irrelevant, but needs clarification" -# expect( subject.trail_stop_price ).to be_nil.or be_zero -# end + its( :commission){ is_expected.to be_a( BigDecimal ).and be > 0 } +# its( :average_fill_price ){ is_expected.not_to be_nil.or be_zero } +# its( :average_fill_price ){is_expected.to be_a BigDecimal } + its( :status ) { is_expected.to eq 'Filled' } +# it "mysterious trailing stop price is absent", pending: true do +# pending "seems to be irrelevant, but needs clarification" +# expect( subject.trail_stop_price ).to be_nil.or be_zero +# end end @@ -182,10 +205,9 @@ def remove_open_orders its( :request_id){ is_expected.to eq( OPTS[:connection][:request_id] ).or eq(-1) } its( :contract){ is_expected.to eq contract } - it " has meaningful attributes " do + it " has meaningful attributes " do exec = subject.execution expect( exec.perm_id).to be_an Integer - expect( exec.client_id).to eq( OPTS[:connection][:client_id] ).or be_zero expect( exec.local_id).to be_an Integer expect( exec.exec_id).to be_a String expect( exec.time).to be_a DateTime @@ -194,33 +216,29 @@ def remove_open_orders expect( exec.side).to eq side expect( exec.shares).to eq order.total_quantity expect( exec.cumulative_quantity).to eq order.total_quantity - expect( exec.price).to be > 1 # assuming EUR/USD stays in the range 1 --- 2 + expect( exec.price).to be > 1 # assuming EUR/USD stays in the range 1 --- 2 expect( exec.price).to be < 2 expect( exec.price).to eq exec.average_price expect( exec.liquidation).to be_falsy - end + end end # parameter pnl: true: there is a realized pnl # takes the last ExecutionData-record as reference for exec_id - RSpec.shared_examples 'Valid CommissionReport' do | pnl | - it{ is_expected.to be_an IB::Messages::Incoming::CommissionReport } - # data.keys: [:version, :exec_id, :commission, :currency, :realized_pnl, :yield, :yield_redemption_date] - it " has a proper execution id" do - e= IB::Connection.current.received[:ExecutionData].last.execution.exec_id - expect( subject.exec_id ).to eq e - end - its( :commission ){is_expected.to be_a BigDecimal} - its( :currency ){ is_expected.to eq OPTS[:connection][:base_currency] } - its( :yield ){ is_expected.to be_nil } - its( :yield_redemption_date){ is_expected.to be_nil} # no date, YYYYMMDD format for bonds - if pnl>0 - its( :realized_pnl ){is_expected.to be_a BigDecimal} - else - its( :realized_pnl ){is_expected.to be_nil} - end - - end + RSpec.shared_examples 'Valid CommissionReport' do | pnl | + it{ is_expected.to be_an IB::Messages::Incoming::CommissionReport } + # data.keys: [:version, :exec_id, :commission, :currency, :realized_pnl, :yield, :yield_redemption_date] + it " has a proper execution id" do + e= IB::Connection.current.received[:ExecutionData].last.execution.exec_id + expect( subject.exec_id ).to eq e + end + its( :commission ){is_expected.to be_a BigDecimal} + its( :currency ){ is_expected.to eq OPTS[:connection][:base_currency] } + its( :yield ){ is_expected.to be_nil } + its( :yield_redemption_date){ is_expected.to be_nil} # no date, YYYYMMDD format for bonds + its( :realized_pnl ){is_expected.to be_a( BigDecimal ).or be_nil} + + end =begin @@ -232,7 +250,7 @@ def remove_open_orders it 'receives all appropriate response messages' do - ib = IB::Connection.current + ib = IB::Connection.current ib.received[:OpenOrder].should have_at_least(1).order_message ib.received[:OrderStatus].should have_at_least(1).status_message end diff --git a/spec/spec.yml b/spec/spec.yml index 1d96212..e9f5861 100644 --- a/spec/spec.yml +++ b/spec/spec.yml @@ -1,17 +1,18 @@ --- :connection: - :port: 4002 # 7497 or 4001 / 7496 - :host: 127.0.0.1 + :port: 7497 # 4002 # 7497 or 4001 / 7496 + :host: 127.0.0.1 + # :host: 10.247.8.109 #10.247.8.109 # 127.0.0.1 # :client_id: 2111 # if commented: use a randomy choosen id instead :base_currency: EUR :reuters: false # currently not used - :account: DU167348 # Set this to your Paper Account Number + :account: DU4035278 #U7274612 # DU4035278 # Set this to your Paper Account Number :market_data: false # if true: include tests depending on market-data subscriptions for the sample -:stock: +:stock: # SAMPLE Stock in tests :symbol: 'GE' :currency: 'USD' # optional :exchange: 'SMART' # optional - :con_id: 7516 # optional + :con_id: 498843743 # optional diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a7c61d5..d1aedfc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ require 'bundler/setup' require 'rspec' require 'rspec/its' +require 'rspec/given' require 'rspec/collection_matchers' require 'ib-api' require 'pp' @@ -16,7 +17,7 @@ # read items from spec.yml read_yml = -> (key) do - YAML::load_file( File.expand_path('../spec.yml',__FILE__))[key] + YAML::load_file( File.expand_path('../spec.yml',__FILE__))[key] end @@ -24,28 +25,28 @@ OPTS[:connection] = read_yml[:connection] ACCOUNT = OPTS[:connection][:account] # shortcut for active account (orders portfolio_values ect.) SAMPLE = IB::Stock.new read_yml[:stock] - + RSpec.configure do |config| puts "Running specs with OPTS:" pp OPTS - # ermöglicht die Einschränkung der zu testenden Specs - # durch >>it "irgendwas", :focus => true do << - # - # - #This configuration allows you to filter to specific examples or groups by tagging - #them with :focus metadata. When no example or groups are focused (which should be - #the norm since it's intended to be a temporary change), the filter will be ignored. - # + # ermöglicht die Einschränkung der zu testenden Specs + # durch >>it "irgendwas", :focus => true do << + # + # + #This configuration allows you to filter to specific examples or groups by tagging + #them with :focus metadata. When no example or groups are focused (which should be + #the norm since it's intended to be a temporary change), the filter will be ignored. + # #config.filter_run_including focus: true - - #RSpec also provides aliases--fit, fdescribe and fcontext--as a shorthand for - #it, describe and context with :focus metadata, making it easy to temporarily - #focus an example or group by prefixing an f. + + #RSpec also provides aliases--fit, fdescribe and fcontext--as a shorthand for + #it, describe and context with :focus metadata, making it easy to temporarily + #focus an example or group by prefixing an f. config.filter_run_when_matching focus: true - config.alias_it_should_behave_like_to :it_has_message, 'has message:' - config.expose_dsl_globally = true #+ monkey-patching in rspec 3 - config.order = 'defined' # "random" + config.alias_it_should_behave_like_to :it_has_message, 'has message:' + config.expose_dsl_globally = true #+ monkey-patching in rspec 3 + config.order = 'defined' # "random" end diff --git a/spec/support/array_spec.rb b/spec/support/array_spec.rb new file mode 100644 index 0000000..044818f --- /dev/null +++ b/spec/support/array_spec.rb @@ -0,0 +1,33 @@ +require "spec_helper" + +#class Array +# include Support::ArrayFunction +#end + +describe "Support::ArrayFunction" do + describe "insert an entry" do + Given( :the_array ) { [ ] } + Given( :the_entry ){ { a: 2, c: 2 } } + When { the_array.save_insert the_entry, :a } + Then { the_array == [ { a: 2, c: 2 } ] } + end + describe "insert another entry" do + Given( :the_array ) { [ { a: 1, b: 2 } ] } + Given( :the_entry ){ { a: 2, c: 2 } } + When { the_array.save_insert the_entry, :a } + Then { the_array == [ { a: 1, b: 2 }, { a: 2, c: 2 } ] } + end + describe "overwrite the entry" do + Given( :the_array ) { [ { a: 1, b: 2 } , { a: 2, c: 2 } ] } + Given( :the_entry ){ { a: 2, c: 3 } } + When { the_array.save_insert the_entry, :a } + Then { the_array == [ { a: 1, b: 2 }, { a: 2, c: 3 } ] } + end + describe "keep the entry" do + Given( :the_array ) { [ { a: 1, b: 2 } , { a: 2, c: 2 } ] } + Given( :the_entry ){ { a: 2, c: 3 } } + When { the_array.save_insert the_entry, :a, false } + Then { the_array == [ { a: 1, b: 2 }, { a: 2, c: 2 } ] } + end +end + diff --git a/update.md b/update.md new file mode 100644 index 0000000..2d165fc --- /dev/null +++ b/update.md @@ -0,0 +1,71 @@ +# Update from 9.72 to 10.12 # Aug 2022 + +Protocol of api-changes: https://www.interactivebrokers.com/en/index.php?f=24356 + +1. Update Incoming and outgoing Messages constants + lib/ib/messages/incoming.rb + lib/ib/massages/outgoing.rb + + note: Field types : int = 1 + string = 2 + float = 2 + python/message.py + +2. Update ServerVersion + lib/ib/server_versions.rb + New supported server Version: 165 /:min_server_er_historical_schedule/ + + python/server_versions.py + + +3. Update ClientVersion + is NOT necessary. According to EClient.java, ClientVersion 66 is the most recent one + + ToDo: Can we omit the constant at all? + +4. Update Max_Version --: 165 + in lib/ib/server_versions.rb + +5. Start of a console with a running papertrading account + +> BUFFER:: "\x00\x00\x00\x1A165\x0020220812 06:42:26 GMT\x00" +> F: Connected to server, version: 165, using client-id: 2000, +> connection time: 2022-08-12 06:42:27 +0000 local, 2022-08-12T06:42:26+00:00 remote. +> BUFFER:: "\x00\x00\x00B15\x001\x00DF4035274,DU4035275,DU4035276,DU4035277,DU4035278,DU4035279,\x00" +> F: Connected to server, version: 165, using client-id: 2000, +> connection time: 2022-08-12 06:42:27 +0000 local, 2022-08-12T06:42:26+00:00 remote. +> BUFFER:: "\x00\x00\x00B15\x001\x00DF4035274,DU4035275,DU4035276,DU4035277,DU4035278,DU4035279,\x00" +> I: < ManagedAccounts: DF4035274 - DU4035275 - DU4035276 - DU4035277 - DU4035278 - DU4035279> +> BUFFER:: "\x00\x00\x00\x069\x001\x001\x00" +> I: Got next valid order id: 1. +> Connection established on Port 4002, client_id 2000 used + +**The login-procedure is unchanged!** + +6. Starting with tests + +6.1 connect_spec.rb + After editing connect.yml the test passes + +6.2 ContractDetails + Changes: a) Version is not transmitted anymore + b) contract_detail.md_size_multiplier is not used anymore + c) remaining buffer: (stock) => ["COMMON", "1", "1", "100"], + --:: added stock_type, min_size, size_increment, suggested_size_increment + to lib/ib/model/ContractDetail.rb and /lib/ib/messages/incoming/contract_data.rb + + Ensure that the buffer is read completely! + +6.3 HeadTimeStamp --:: OK +6.4 HistogramData --:: OK +6.4 HistoricalData --:: OK +6.5 ManagedAccounts--:: OK +6.6 OptionChian --:: OK +6.7 PositionData --:: OK +6.8 PositionsMulti --:: OK +6.9 ReceiveFA --:: OK +6.10 MultiAccountUpdate --:: OK + + + +