From 3417869bb08e51e512d391cd699f4dbd35a64d84 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 7 Oct 2021 13:48:03 +0000 Subject: [PATCH 01/76] Imports --- lib/ib/messages/incoming/historical_data.rb | 26 +++++++++++++++++++-- lib/ib/messages/outgoing.rb | 3 ++- lib/ib/server_versions.rb | 18 +++++++++----- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/lib/ib/messages/incoming/historical_data.rb b/lib/ib/messages/incoming/historical_data.rb index 7d924a0..bc173da 100644 --- a/lib/ib/messages/incoming/historical_data.rb +++ b/lib/ib/messages/incoming/historical_data.rb @@ -96,8 +96,30 @@ 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 diff --git a/lib/ib/messages/outgoing.rb b/lib/ib/messages/outgoing.rb index e125f8b..ab20d4d 100644 --- a/lib/ib/messages/outgoing.rb +++ b/lib/ib/messages/outgoing.rb @@ -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 @@ -343,6 +343,7 @@ module Outgoing 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 diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index 2f45fe7..92f64f4 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -99,16 +99,22 @@ :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 } # 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_duration] # imessages/outgoing/request_tick_Data is prepared for change to ver. 140 , its commented for now From 4a4b32c4a3df2e914cfe5f1e4dade0ba499a6891 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 24 Dec 2021 11:55:44 +0000 Subject: [PATCH 02/76] Preparations for Vers. 10 (1) --- lib/ib/constants.rb | 3 +- .../messages/outgoing/request_market_depth.rb | 56 +++++++++++++++++++ lib/models/ib/contract.rb | 5 ++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 lib/ib/messages/outgoing/request_market_depth.rb diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index 1685451..8a841ee 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -231,7 +231,8 @@ module IB 'BILL' => :bill, 'BSK' => :basket, 'FWD' => :forward, - 'FIXED' => :fixed }.freeze + 'FIXED' => :fixed , + 'CRYPTO' => :crypto }.freeze # Obtain symbolic value from given property code: # VALUES[:side]['B'] -> :buy 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..cca80fd --- /dev/null +++ b/lib/ib/messages/outgoing/request_market_depth.rb @@ -0,0 +1,56 @@ + +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 + # + # + CancelMarketDepth = def_message([11, 1], :is_smart_depth) + + 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 + + [ + self.class.message_id, + self.class.version, + @data[:request_id], + contract.con_id, + contract.symbol, + contract[:sec_type], + contract.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/models/ib/contract.rb b/lib/models/ib/contract.rb index 810684d..77f8b3b 100644 --- a/lib/models/ib/contract.rb +++ b/lib/models/ib/contract.rb @@ -375,6 +375,11 @@ 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 " From 8edea5f444820f77546725ee52f2f1c24c0c4d1a Mon Sep 17 00:00:00 2001 From: NewDevel Date: Fri, 12 Aug 2022 04:47:28 +0000 Subject: [PATCH 03/76] Update Messages and ServerVersions --- Gemfile.lock | 79 +++++++++++++++++-------------------- lib/ib/messages/incoming.rb | 9 ++++- lib/ib/messages/outgoing.rb | 8 +++- lib/ib/server_versions.rb | 9 ++++- update.md | 17 ++++++++ 5 files changed, 77 insertions(+), 45 deletions(-) create mode 100644 update.md diff --git a/Gemfile.lock b/Gemfile.lock index bec9bc3..046a3c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,39 +1,34 @@ -GIT - remote: https://github.com/ohler55/ox.git - revision: 67ce6ecb45a0d1354e1f8ed9a155826ba986e21e - specs: - ox (2.13.4) - PATH remote: . specs: - ib-api (972.2) + ib-api (972.5) activemodel activesupport (>= 6.0) + ox + terminal-table GEM remote: https://rubygems.org/ specs: - activemodel (6.1.2.1) - activesupport (= 6.1.2.1) - activesupport (6.1.2.1) + activemodel (7.0.3.1) + activesupport (= 7.0.3.1) + activesupport (7.0.3.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) + concurrent-ruby (1.1.10) + diff-lcs (1.5.0) + ffi (1.15.5) + formatador (1.1.0) + guard (2.18.0) 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) + pry (>= 0.13.0) shellany (~> 0.0) thor (>= 0.18.1) guard-compat (1.2.1) @@ -41,64 +36,64 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - i18n (1.8.9) + i18n (1.12.0) concurrent-ruby (~> 1.0) - listen (3.2.1) + listen (3.7.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) + minitest (5.16.2) nenv (0.3.0) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - pry (0.13.1) + ox (2.14.11) + pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) - rake (13.0.1) - rb-fsevent (0.10.4) + rake (13.0.6) + rb-fsevent (0.11.1) 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 (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.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) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) + rspec-support (~> 3.11.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.9.1) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.3) + rspec-support (~> 3.11.0) + rspec-support (3.11.0) shellany (0.0.1) - thor (1.0.1) - tzinfo (2.0.4) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + thor (1.2.1) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) - value_semantics (3.6.0) - zeitwerk (2.4.2) + unicode-display_width (2.2.0) PLATFORMS - ruby + x86_64-linux DEPENDENCIES bundler guard guard-rspec ib-api! - ox! rake (~> 13.0) rspec rspec-collection_matchers rspec-its - value_semantics BUNDLED WITH - 1.17.3 + 2.3.7 diff --git a/lib/ib/messages/incoming.rb b/lib/ib/messages/incoming.rb index 66a57b5..0543741 100644 --- a/lib/ib/messages/incoming.rb +++ b/lib/ib/messages/incoming.rb @@ -248,4 +248,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/outgoing.rb b/lib/ib/messages/outgoing.rb index ab20d4d..2d35f15 100644 --- a/lib/ib/messages/outgoing.rb +++ b/lib/ib/messages/outgoing.rb @@ -437,4 +437,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/server_versions.rb b/lib/ib/server_versions.rb index 92f64f4..6543181 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -109,7 +109,14 @@ :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_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 } # 100+ messaging */ # 100 = enhanced handshake, msg length prefixes diff --git a/update.md b/update.md new file mode 100644 index 0000000..598d2d5 --- /dev/null +++ b/update.md @@ -0,0 +1,17 @@ +# Update from 9.72 to 10.12 # Aug 2022 + +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 ServerVersions + lib/ib/server_versions.rb + New supported server Version: 165 /:min_server_er_historical_schedule/ + + python/server_versions.py + From 7c353aeaaf528fb5e12dcf174255cd3f99719e0e Mon Sep 17 00:00:00 2001 From: NewDevel Date: Fri, 12 Aug 2022 09:55:07 +0000 Subject: [PATCH 04/76] Adapt ContractData Message --- lib/ib/messages.rb | 2 +- lib/ib/messages/incoming/contract_data.rb | 10 +++-- lib/ib/server_versions.rb | 4 +- lib/models/ib/contract_detail.rb | 4 ++ spec/ib/connect_spec.rb | 2 +- .../incoming/abstract_message_spec.rb | 7 ++- .../ib/messages/incoming/account_info_spec.rb | 6 +-- .../messages/incoming/account_summary_spec.rb | 5 +-- .../incoming/account_update_multi_spec.rb | 10 ++--- spec/ib/messages/incoming/alert_spec.rb | 2 +- .../messages/incoming/contract_data_spec.rb | 7 ++- spec/main_helper.rb | 6 +-- update.md | 45 ++++++++++++++++++- 13 files changed, 77 insertions(+), 33 deletions(-) diff --git a/lib/ib/messages.rb b/lib/ib/messages.rb index ad1fa90..10ed272 100644 --- a/lib/ib/messages.rb +++ b/lib/ib/messages.rb @@ -96,4 +96,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/incoming/contract_data.rb b/lib/ib/messages/incoming/contract_data.rb index 4d17a66..76662d6 100644 --- a/lib/ib/messages/incoming/contract_data.rb +++ b/lib/ib/messages/incoming/contract_data.rb @@ -6,7 +6,7 @@ module ContractAccessors end ContractDetails = ContractData = - def_message([10, [6, 8]], + def_message([ 10, 0 ], #, [8, 8]], [:request_id, :int], ## request id [:contract, :symbol, :string], ## next the major contract-fields [:contract, :sec_type, :string], ## are transmitted @@ -20,7 +20,7 @@ module ContractAccessors [:contract, :trading_class, :string], ## new Version 8 [:contract, :con_id, :int], [:contract_detail, :min_tick, :decimal], - [:contract_detail, :md_size_multiplier, :int], +# [:contract_detail, :md_size_multiplier, :int], # Vers 10.12 not used anymore [:contract, :multiplier, :int], [:contract_detail, :order_types, :string], [:contract_detail, :valid_exchanges, :string], @@ -42,7 +42,11 @@ module ContractAccessors [:contract_detail, :under_symbol, :string ], [:contract_detail, :under_sec_type, :string ], [:contract_detail, :market_rule_ids, :string ], - [:contract_detail, :real_expiration_date, :date ] + [:contract_detail, :real_expiration_date, :date ], + [:contract_detail, :stock_type, :string ], # new Version 10.12 + [:contract_detail, :min_size, :int ], # new Version 10.12 + [:contract_detail, :size_increment, :int ], # new Version 10.12 + [:contract_detail, :suggested_size_increment, :int ], # new Version 10.12 ) # # diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index 6543181..b374cda 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -122,6 +122,6 @@ # 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_duration] +MAX_CLIENT_VER = 165 #known_servers[:min_server_ver_d_peg_orders] +MAX_CLIENT_VER = known_servers[:min_server_ver_historical_schedule] # imessages/outgoing/request_tick_Data is prepared for change to ver. 140 , its commented for now diff --git a/lib/models/ib/contract_detail.rb b/lib/models/ib/contract_detail.rb index 9372cd1..5d8ad76 100644 --- a/lib/models/ib/contract_detail.rb +++ b/lib/models/ib/contract_detail.rb @@ -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. diff --git a/spec/ib/connect_spec.rb b/spec/ib/connect_spec.rb index c843786..5f2a482 100644 --- a/spec/ib/connect_spec.rb +++ b/spec/ib/connect_spec.rb @@ -4,7 +4,7 @@ before(:all){ establish_connection } after(:all) { close_connection } - + context "A new connection" do it{ expect( IB::Connection.current ).to be_a IB::Connection } end diff --git a/spec/ib/messages/incoming/abstract_message_spec.rb b/spec/ib/messages/incoming/abstract_message_spec.rb index 80ffe7c..0c6c7fb 100644 --- a/spec/ib/messages/incoming/abstract_message_spec.rb +++ b/spec/ib/messages/incoming/abstract_message_spec.rb @@ -8,7 +8,7 @@ its( :version ) { is_expected.to eq 1 } its( :data ) { is_expected.not_to be_empty } its( :buffer ) { is_expected.to be_empty } -end +end RSpec.describe IB::Messages::Incoming do @@ -43,13 +43,12 @@ context "without value" do subject{ int_instruction.new ["1"] } it_behaves_like 'simple_instruction' - its(:the_integer){ is_expected.to be_nil } - + 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 } + its(:the_integer){ is_expected.to be_nil } end end context "Instruction with String" do diff --git a/spec/ib/messages/incoming/account_info_spec.rb b/spec/ib/messages/incoming/account_info_spec.rb index 2260ea0..eb1d629 100644 --- a/spec/ib/messages/incoming/account_info_spec.rb +++ b/spec/ib/messages/incoming/account_info_spec.rb @@ -51,16 +51,14 @@ establish_connection ib = IB::Connection.current ib.send_message :RequestAccountData, :subscribe => true, :account_code => ACCOUNT - ib.wait_for :PortfolioValue 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 } + let( :the_portfolio_value ){ IB::Connection.current.received[:PortfolioValue].first } end it_behaves_like 'Account Value Message' do @@ -68,7 +66,7 @@ end it_behaves_like 'Valid AccountValue Object' do - let( :the_account_value_object ){ IB::Connection.current.received[:AccountValue].first.account_value } + 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..df65e70 100644 --- a/spec/ib/messages/incoming/account_summary_spec.rb +++ b/spec/ib/messages/incoming/account_summary_spec.rb @@ -22,7 +22,6 @@ establish_connection ib = IB::Connection.current req_id= ib.send_message :RequestAccountSummary, tags: 'RegTMargin,ExcessLiquidity, DayTradesRemaining' - ib.wait_for :AccountSummary sleep 1 ib.send_message :CancelAccountSummary, id: req_id @@ -32,11 +31,11 @@ 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 } + 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 diff --git a/spec/ib/messages/incoming/account_update_multi_spec.rb b/spec/ib/messages/incoming/account_update_multi_spec.rb index b1bafae..edf3d34 100644 --- a/spec/ib/messages/incoming/account_update_multi_spec.rb +++ b/spec/ib/messages/incoming/account_update_multi_spec.rb @@ -1,6 +1,6 @@ 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 } @@ -25,16 +25,14 @@ ib = IB::Connection.current request_id =ib.send_message :RequestAccountUpdatesMulti #, account: 'ALL' is default ib.wait_for :AccountUpdatesMulti, 10 - sleep 0.1 + 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..f851b08 100644 --- a/spec/ib/messages/incoming/alert_spec.rb +++ b/spec/ib/messages/incoming/alert_spec.rb @@ -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..6a3e336 100644 --- a/spec/ib/messages/incoming/contract_data_spec.rb +++ b/spec/ib/messages/incoming/contract_data_spec.rb @@ -19,17 +19,16 @@ after(:all){ IB::Connection.current.clear_received :ContractData } - # it_behaves_like 'ContractData Message' do -# let( :the_message ){ IB::Connection.current.received[:ContractData].first } +# let( :the_message ){ IB::Connection.current.received[:ContractData].first } # end - context "Basics" do + 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} + its( :con_id ){is_expected.to eq 498843743} end context "received a single contract" do diff --git a/spec/main_helper.rb b/spec/main_helper.rb index 48c9ca1..90f5098 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -43,10 +43,10 @@ def establish_connection raise "Unable to verify IB PAPER ACCOUNT" unless ib.received?(:ManagedAccounts) - received = ib.received[:ManagedAccounts].first.accounts_list.split(',') - unless received.include?(ACCOUNT) + accounts = ib.received[:ManagedAccounts].first.accounts_list.split(',') + unless accounts.include?(ACCOUNT) close_connection - raise "Connected to wrong account #{received}, expected #{ACCOUNT}" + 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 diff --git a/update.md b/update.md index 598d2d5..16bd80e 100644 --- a/update.md +++ b/update.md @@ -1,5 +1,7 @@ # 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 @@ -9,9 +11,50 @@ float = 2 python/message.py -2. Update ServerVersions +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! + + + From edafd55e8072e6ab47764da61d8acdfd34eebdbf Mon Sep 17 00:00:00 2001 From: NewDevel Date: Fri, 12 Aug 2022 13:36:22 +0000 Subject: [PATCH 05/76] Tests of ib-api are passing --- lib/ib/server_versions.rb | 2 +- spec/spec.yml | 4 ++-- update.md | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index b374cda..8784cfb 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -122,6 +122,6 @@ # 100 = enhanced handshake, msg length prefixes MIN_CLIENT_VER = 100 -MAX_CLIENT_VER = 165 #known_servers[:min_server_ver_d_peg_orders] +#MAX_CLIENT_VER = 165 #known_servers[:min_server_ver_d_peg_orders] MAX_CLIENT_VER = known_servers[:min_server_ver_historical_schedule] # imessages/outgoing/request_tick_Data is prepared for change to ver. 140 , its commented for now diff --git a/spec/spec.yml b/spec/spec.yml index 1d96212..91abd12 100644 --- a/spec/spec.yml +++ b/spec/spec.yml @@ -5,13 +5,13 @@ # :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 # Set this to your Paper Account Number :market_data: false # if true: include tests depending on market-data subscriptions for the sample :stock: :symbol: 'GE' :currency: 'USD' # optional :exchange: 'SMART' # optional - :con_id: 7516 # optional + :con_id: 498843743 # optional diff --git a/update.md b/update.md index 16bd80e..2d165fc 100644 --- a/update.md +++ b/update.md @@ -56,5 +56,16 @@ Protocol of api-changes: https://www.interactivebrokers.com/en/index.php?f=2435 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 + + From e097b3142a9cd0e55dcdeab8f193fe90aa5903da Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 25 Aug 2022 15:55:35 +0000 Subject: [PATCH 06/76] Updates in incoming ticks-protocol --- .gitignore | 2 + lib/ib/connection.rb | 8 +- lib/ib/messages/incoming/ticks.rb | 4 +- .../messages/outgoing/request_market_depth.rb | 4 +- lib/ib/socket.rb | 186 +++++++++--------- 5 files changed, 104 insertions(+), 100 deletions(-) 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/lib/ib/connection.rb b/lib/ib/connection.rb index 49853c6..b9562d6 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -119,7 +119,7 @@ def connect 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 | + socket.decode_message( socket.receive_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 @@ -412,9 +412,9 @@ def subscribers 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 + socket.decode_message( socket.receive_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]})"} diff --git a/lib/ib/messages/incoming/ticks.rb b/lib/ib/messages/incoming/ticks.rb index 21fb20e..a447220 100644 --- a/lib/ib/messages/incoming/ticks.rb +++ b/lib/ib/messages/incoming/ticks.rb @@ -116,10 +116,10 @@ def valid? # :theta - The option theta value. # :under_price - The price of the underlying. TickOptionComputation = TickOption = - def_message([21, 6], AbstractTick, + def_message([21, 0], AbstractTick, [:ticker_id, :int], [:tick_type, :int], - # What is the "not yet computed" indicator: + [:tick_attribute, :int], [:implied_volatility, :decimal_limit_1], # -1 and below [:delta, :decimal_limit_2], # -2 and below [:option_price, :decimal_limit_1], # -1 -"- diff --git a/lib/ib/messages/outgoing/request_market_depth.rb b/lib/ib/messages/outgoing/request_market_depth.rb index cca80fd..9c821ce 100644 --- a/lib/ib/messages/outgoing/request_market_depth.rb +++ b/lib/ib/messages/outgoing/request_market_depth.rb @@ -27,6 +27,8 @@ def encode @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, @@ -35,7 +37,7 @@ def encode contract.con_id, contract.symbol, contract[:sec_type], - contract.last_trade_date_or_contract_month, + contract.expiry, #last_trade_date_or_contract_month, contract.strike, contract.right == :none ? '' : contract.right, contract.multiplier, diff --git a/lib/ib/socket.rb b/lib/ib/socket.rb index 3fc97e8..3a6d100 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -3,11 +3,11 @@ module IBSupport refine Array do def tws if blank? - nil.tws + nil.tws else - self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil] - # elemets with empty array's are cut - # this is the desired behavior! + self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil] + # elemets with empty array's are cut + # this is the desired behavior! end end end @@ -19,9 +19,9 @@ def tws refine String do def tws if empty? - IB::EOL + IB::EOL else - self[-1] == IB::EOL ? self : self+IB::EOL + self[-1] == IB::EOL ? self : self+IB::EOL end end end @@ -46,68 +46,68 @@ def tws refine NilClass do def tws - IB::EOL + 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 - # + # 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 + # 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 @@ -117,17 +117,17 @@ def initialising_handshake 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 +135,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 +155,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(4096)[0] + # STDOUT.puts "BUFFER:: #{buffer.inspect}" + complete_message_buffer << buffer + + end while buffer.size == 4096 + 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 IBSocket end # module IB From b0f0a385c595537fd8679b040fa5552c59994d06 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 8 Dec 2022 10:27:56 +0100 Subject: [PATCH 07/76] class extentions need to be required relative --- lib/requires.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/requires.rb b/lib/requires.rb index 67c6632..36d3c76 100644 --- a/lib/requires.rb +++ b/lib/requires.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/module/attribute_accessors.rb' -require 'extensions/class-extensions' +require_relative 'extensions/class-extensions' require 'terminal-table' From 5540353d8f7f28ad9237a478d292f3c77dfeda45 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sun, 15 Jan 2023 12:04:23 +0000 Subject: [PATCH 08/76] Adaption of request_marketdata to recent TWS-Versions --- Gemfile.lock | 99 ------------------- .../messages/outgoing/request_marketdata.rb | 2 +- 2 files changed, 1 insertion(+), 100 deletions(-) delete mode 100644 Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 046a3c6..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,99 +0,0 @@ -PATH - remote: . - specs: - ib-api (972.5) - activemodel - activesupport (>= 6.0) - ox - terminal-table - -GEM - remote: https://rubygems.org/ - specs: - activemodel (7.0.3.1) - activesupport (= 7.0.3.1) - activesupport (7.0.3.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) - coderay (1.1.3) - concurrent-ruby (1.1.10) - diff-lcs (1.5.0) - ffi (1.15.5) - formatador (1.1.0) - guard (2.18.0) - formatador (>= 0.2.4) - listen (>= 2.7, < 4.0) - lumberjack (>= 1.0.12, < 2.0) - nenv (~> 0.1) - notiffany (~> 0.0) - pry (>= 0.13.0) - 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.12.0) - concurrent-ruby (~> 1.0) - listen (3.7.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.16.2) - nenv (0.3.0) - notiffany (0.1.3) - nenv (~> 0.1) - shellany (~> 0.0) - ox (2.14.11) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - rake (13.0.6) - rb-fsevent (0.11.1) - rb-inotify (0.10.1) - ffi (~> 1.0) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-collection_matchers (1.2.0) - rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-its (1.3.0) - rspec-core (>= 3.0.0) - rspec-expectations (>= 3.0.0) - rspec-mocks (3.11.1) - diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - shellany (0.0.1) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - thor (1.2.1) - tzinfo (2.0.5) - concurrent-ruby (~> 1.0) - unicode-display_width (2.2.0) - -PLATFORMS - x86_64-linux - -DEPENDENCIES - bundler - guard - guard-rspec - ib-api! - rake (~> 13.0) - rspec - rspec-collection_matchers - rspec-its - -BUNDLED WITH - 2.3.7 diff --git a/lib/ib/messages/outgoing/request_marketdata.rb b/lib/ib/messages/outgoing/request_marketdata.rb index a3c97d0..758874c 100644 --- a/lib/ib/messages/outgoing/request_marketdata.rb +++ b/lib/ib/messages/outgoing/request_marketdata.rb @@ -96,7 +96,7 @@ module Outgoing [:tick_list, ->(tick_list){ tick_list.is_a?(Array) ? tick_list.join(',') : (tick_list || '')}, []], [:snapshot, false], [:regulatory_snapshot, false], - [:mkt_data_options, "XYZ"] + [:mkt_data_options, ""] # changed to enable requests in V 10.19 ff end end end From cd1e103301615cde1d50c55dd16580be2be2bf75 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sun, 22 Jan 2023 21:13:16 +0100 Subject: [PATCH 09/76] Upate for Hostorical- and Realtimedata in V10.19ff --- lib/ib/messages/outgoing/bar_requests.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/ib/messages/outgoing/bar_requests.rb b/lib/ib/messages/outgoing/bar_requests.rb index 27c4128..19b36fa 100644 --- a/lib/ib/messages/outgoing/bar_requests.rb +++ b/lib/ib/messages/outgoing/bar_requests.rb @@ -62,15 +62,12 @@ 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 @@ -189,7 +186,7 @@ def encode 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. + '' # chartOptions:TagValueList - For internal use only. Use default value XYZ. ] end end # RequestHistoricalData From 6a5c0deec68c5357df6b5dc547ac59e5b6695385 Mon Sep 17 00:00:00 2001 From: Moritz Kork <35692507+moritzkork@users.noreply.github.com> Date: Fri, 10 Feb 2023 17:01:36 +0100 Subject: [PATCH 10/76] Update connection.rb patches to run in windows - line [287] added check for windows environment and reinstated line [288], commenting it out led to stall in Windows environment - line [292], [306] added check for windows environment, ingores TWS shutdown check in windows environment (functionality does not work in Windows environment) --- lib/ib/connection.rb | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index b9562d6..8862410 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -284,23 +284,27 @@ 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 + if windows_ver = RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/) + process_message if select [socket], nil, nil, time_left + end + # 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 windows_ver.nil? + 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 end rescue Errno::ECONNRESET => e logger.fatal e.message From 6736df624bb3befdf23ecccc3c1f5846d8fbfe6a Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 16 Feb 2023 05:31:37 +0100 Subject: [PATCH 11/76] update connection.rb, refactoring method process_messages --- lib/ib/connection.rb | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 8862410..5302d8b 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -284,28 +284,26 @@ 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 - if windows_ver = RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/) + # 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 - end + 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 windows_ver.nil? 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. + # 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). + # 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. + # 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 - end + end # if + end # if + end # while rescue Errno::ECONNRESET => e logger.fatal e.message if e.message =~ /Connection reset by peer/ From 91c3f9087f44b8f80161e0641938561b8b7bf0ef Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 28 Feb 2023 06:03:14 +0000 Subject: [PATCH 12/76] update server-versions --- lib/ib/server_versions.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index 8784cfb..41cb4b3 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -117,6 +117,21 @@ :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 + + + } # 100+ messaging */ # 100 = enhanced handshake, msg length prefixes From 17dd5b1b4f85578400be19c11cb0f62f51c6322e Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 28 Feb 2023 06:11:11 +0000 Subject: [PATCH 13/76] update server-versions, inserted necessary kommas --- lib/ib/server_versions.rb | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index 41cb4b3..4925c07 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -117,21 +117,18 @@ :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_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 - - - } # 100+ messaging */ # 100 = enhanced handshake, msg length prefixes From f7d5108fb901152a94fd5975abd2a05f62a95477 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 9 Mar 2023 19:50:42 +0100 Subject: [PATCH 14/76] correct server-versions --- lib/ib/server_versions.rb | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/ib/server_versions.rb b/lib/ib/server_versions.rb index 4925c07..74b0fd3 100644 --- a/lib/ib/server_versions.rb +++ b/lib/ib/server_versions.rb @@ -116,19 +116,19 @@ :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_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 } # 100+ messaging */ # 100 = enhanced handshake, msg length prefixes From ff184b6baaee3c4af12c4811d4bdd6b68d46c14e Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 1 Apr 2024 17:30:41 +0200 Subject: [PATCH 15/76] refactoring of class-extensions (with tests) --- Gemfile | 1 + lib/extensions/class-extensions.rb | 130 ++++++++++++++++------------- spec/ib/extensions_spec.rb | 56 +++++++++++++ spec/spec_helper.rb | 1 + 4 files changed, 130 insertions(+), 58 deletions(-) create mode 100644 spec/ib/extensions_spec.rb 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/lib/extensions/class-extensions.rb b/lib/extensions/class-extensions.rb index 7a8b1b3..26ba2a4 100644 --- a/lib/extensions/class-extensions.rb +++ b/lib/extensions/class-extensions.rb @@ -1,4 +1,10 @@ -module CoreExtensions +# Include the method `to_bool` to some basic classes +# +# Prepare the output of arrays via Terminal::Table +# +# Define the method `count_duplicates` for Arrays +# +module ClassExtensions module Array module DuplicatesCounter def count_duplicates @@ -14,78 +20,86 @@ def as_table(&b) 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)} " + + module 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 -end # Time -class Numeric - # Conversion 0/1 into true/false - def to_bool - self == 0 ? false : true + module Numeric + # Conversion 0/1 into true/false + module Bool + def to_bool + self == 0 ? false : true + end + end end -end -class TrueClass - def to_bool - self + module BoolClass + # Conversion 0/1 into true/false + module Bool + def to_bool + self + end + end end -end - -class FalseClass - def to_bool - self + module String + module Bool + 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 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" + module Symbol + module Float + def to_f + 0 + end + end + module Sort + # ActiveModel serialization depends on this method + def <=> other + to_s <=> other.to_s + end 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 -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 +Array.include ClassExtensions::Array::DuplicatesCounter +Array.include ClassExtensions::Array::TablePresenter +FalseClass.include ClassExtensions::BoolClass::Bool +NilClass.include ClassExtensions::BoolClass::Bool +Numeric.include ClassExtensions::Numeric::Bool +Object.include ClassExtensions::Object +String.include ClassExtensions::String::Bool +Symbol.include ClassExtensions::Symbol::Float +Symbol.include ClassExtensions::Symbol::Sort +Time.include ClassExtensions::Time +TrueClass.include ClassExtensions::BoolClass::Bool + + + + + - # 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 diff --git a/spec/ib/extensions_spec.rb b/spec/ib/extensions_spec.rb new file mode 100644 index 0000000..db54894 --- /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/spec_helper.rb b/spec/spec_helper.rb index a7c61d5..21a09b3 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' From 2a6cc6b5b1cead051afa75a2a8043bdabc85d62a Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 1 Apr 2024 18:38:38 +0200 Subject: [PATCH 16/76] Refacturing: IBSupport --> IB::Support --- lib/ib/connection.rb | 2 +- lib/ib/messages/incoming.rb | 2 +- lib/ib/messages/incoming/abstract_message.rb | 2 +- lib/ib/messages/incoming/account_value.rb | 2 +- lib/ib/messages/incoming/contract_data.rb | 2 +- lib/ib/messages/incoming/historical_data.rb | 6 +- lib/ib/messages/incoming/open_order.rb | 2 +- lib/ib/messages/incoming/scanner_data.rb | 2 +- lib/ib/messages/incoming/ticks.rb | 2 +- lib/ib/socket.rb | 62 ++---------------- lib/ib/support.rb | 66 ++++++++++++++++++-- lib/models/ib/condition.rb | 14 ++--- lib/models/ib/contract.rb | 2 +- lib/models/ib/spread.rb | 2 +- 14 files changed, 88 insertions(+), 80 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 5302d8b..442d766 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -19,7 +19,7 @@ class Connection ## public data-queue: received, received?, wait_for, clear_received ## misc: reader_running? - include Support::Logging # provides default_logger + include ::Support::Logging # provides default_logger mattr_accessor :current # Please note, we are realizing only the most current TWS protocol versions, diff --git a/lib/ib/messages/incoming.rb b/lib/ib/messages/incoming.rb index 0543741..39bcf96 100644 --- a/lib/ib/messages/incoming.rb +++ b/lib/ib/messages/incoming.rb @@ -106,7 +106,7 @@ def scan_types [:multiplier, :int] class OptionChainDefinition - using IBSupport # defines tws-method for Array (socket.rb) + using IB::Support # defines tws-method for Array (socket.rb) def load super load_map [:expirations, :array, proc { @buffer.read_date }], diff --git a/lib/ib/messages/incoming/abstract_message.rb b/lib/ib/messages/incoming/abstract_message.rb index 7c1df0d..295f1d3 100644 --- a/lib/ib/messages/incoming/abstract_message.rb +++ b/lib/ib/messages/incoming/abstract_message.rb @@ -4,7 +4,7 @@ module IB module Messages module Incoming - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages # Container for specific message classes, keyed by their message_ids diff --git a/lib/ib/messages/incoming/account_value.rb b/lib/ib/messages/incoming/account_value.rb index f1d098b..58e781b 100644 --- a/lib/ib/messages/incoming/account_value.rb +++ b/lib/ib/messages/incoming/account_value.rb @@ -13,7 +13,7 @@ def accounts end def to_human - "< ManagedAccounts: #{accounts.map(&:account).join(" - ")}>" + "" end end diff --git a/lib/ib/messages/incoming/contract_data.rb b/lib/ib/messages/incoming/contract_data.rb index 76662d6..2cfaec1 100644 --- a/lib/ib/messages/incoming/contract_data.rb +++ b/lib/ib/messages/incoming/contract_data.rb @@ -51,7 +51,7 @@ module ContractAccessors # # class ContractData - using IBSupport # defines tws-method for Array (socket.rb) + using IB::Support # defines tws-method for Array (socket.rb) def contract @contract = IB::Contract.build @data[:contract].merge(:contract_detail => contract_detail) end diff --git a/lib/ib/messages/incoming/historical_data.rb b/lib/ib/messages/incoming/historical_data.rb index bc173da..7c95f1e 100644 --- a/lib/ib/messages/incoming/historical_data.rb +++ b/lib/ib/messages/incoming/historical_data.rb @@ -32,7 +32,7 @@ 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 @@ -67,7 +67,7 @@ def to_human class HistogramData attr_accessor :results - using IBSupport # extended Array-Class from abstract_message + using IB::Support # extended Array-Class from abstract_message def load super @@ -86,7 +86,7 @@ def load class HistoricalDataUpdate attr_accessor :results - using IBSupport # extended Array-Class from abstract_message + using IB::Support # extended Array-Class from abstract_message def bar @bar = IB::Bar.new @data[:bar] diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 9ada7e0..7bf51a7 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -1,7 +1,7 @@ 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) diff --git a/lib/ib/messages/incoming/scanner_data.rb b/lib/ib/messages/incoming/scanner_data.rb index e213263..08236b8 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/ticks.rb b/lib/ib/messages/incoming/ticks.rb index a447220..641c61a 100644 --- a/lib/ib/messages/incoming/ticks.rb +++ b/lib/ib/messages/incoming/ticks.rb @@ -162,7 +162,7 @@ def greeks? ## 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 + using IB::Support # extended Array-Class from abstract_message def resolve_mask @data[:mask].present? ? [ @data[:mask] & 1 , @data[:mask] & 2 ] : [] diff --git a/lib/ib/socket.rb b/lib/ib/socket.rb index 3a6d100..98bd709 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -1,66 +1,16 @@ 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 +require 'ib/support' module IB - # includes methods from IBSupport - # which adds a tws-method to + # includes methods from IB:.Support + # which adds a tws-method to # - Array # - Symbol # - String # - Numeric # - TrueClass, FalseClass and NilClass - # + # module PrepareData - using IBSupport + using IB::Support # First call the method #tws on the data-object # # Then transfom into an Array using the #Pack-Method @@ -111,7 +61,7 @@ def decode_message msg class IBSocket < TCPSocket include PrepareData - using IBSupport + using IB::Support def initialising_handshake v100_prefix = "API".tws.encode 'ascii' diff --git a/lib/ib/support.rb b/lib/ib/support.rb index 173e417..f986b78 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -1,11 +1,19 @@ +# 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 a +# ` +module IB + module Support -module IBSupport refine Array do def zero? false end - # Returns the integer. + # Returns the integer. # retuns nil otherwise or if no element is left on the stack def read_int i= self.shift rescue nil @@ -125,7 +133,7 @@ def read_array hashmode:false, &block # 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? + result = if tags.nil? || tags.flatten.empty? tags else interim = if tags.size.modulo(2).zero? @@ -173,5 +181,55 @@ def read_bar # read a Historical data bar alias read_bool read_boolean - end + + 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 # 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 + end diff --git a/lib/models/ib/condition.rb b/lib/models/ib/condition.rb index 28582be..fa68c08 100644 --- a/lib/models/ib/condition.rb +++ b/lib/models/ib/condition.rb @@ -29,7 +29,7 @@ def serialize class PriceCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # 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 @@ -68,7 +68,7 @@ def self.fabricate contract, operator, price end class TimeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages prop :time def condition_type @@ -105,7 +105,7 @@ def self.fabricate operator, time end class ExecutionCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages def condition_type 5 @@ -133,7 +133,7 @@ def self.fabricate contract end class MarginCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages prop :percent @@ -160,7 +160,7 @@ def self.fabricate operator, percent class VolumeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages prop :volume @@ -193,7 +193,7 @@ def self.fabricate contract, operator, volume end class PercentChangeCondition < OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages prop :percent_change def condition_type @@ -223,7 +223,7 @@ def self.fabricate contract, operator, change end end class OrderCondition - using IBSupport # refine Array-method for decoding of IB-Messages + using IB::Support # refine Array-method for decoding of IB-Messages # subclasses representing specialized condition types. Subclasses = Hash.new(OrderCondition) diff --git a/lib/models/ib/contract.rb b/lib/models/ib/contract.rb index 77f8b3b..2f3fe7c 100644 --- a/lib/models/ib/contract.rb +++ b/lib/models/ib/contract.rb @@ -465,7 +465,7 @@ def table_row class Contract # Contract subclasses representing specialized security types. - using IBSupport + using IB::Support Subclasses = Hash.new(Contract) Subclasses[:bag] = IB::Bag diff --git a/lib/models/ib/spread.rb b/lib/models/ib/spread.rb index d044321..877e5f8 100644 --- a/lib/models/ib/spread.rb +++ b/lib/models/ib/spread.rb @@ -8,7 +8,7 @@ module IB class Spread < Bag has_many :legs - using IBSupport + using IB::Support =begin Parameters: front: YYYMM(DD) From 3d89c2d874206663eeaeee992458c68fcbc38449 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 2 Apr 2024 07:42:52 +0200 Subject: [PATCH 17/76] Separation of Socket & PrepareData, rename IBSocket to IB::Socket --- lib/ib/connection.rb | 4 +-- lib/ib/prepare_data.rb | 59 ++++++++++++++++++++++++++++++++++++++++ lib/ib/socket.rb | 61 +++++------------------------------------- 3 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 lib/ib/prepare_data.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 442d766..d55b27f 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -117,7 +117,7 @@ def connect return end - self.socket = IBSocket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible + self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible socket.initialising_handshake socket.decode_message( socket.receive_messages ) do | the_message | # logger.info{ "TheMessage :: #{the_message.inspect}" } @@ -293,7 +293,7 @@ def process_messages poll_time = 50 # in msec 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] == "" + 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 diff --git a/lib/ib/prepare_data.rb b/lib/ib/prepare_data.rb new file mode 100644 index 0000000..a0e81cf --- /dev/null +++ b/lib/ib/prepare_data.rb @@ -0,0 +1,59 @@ +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") + 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/socket.rb b/lib/ib/socket.rb index 98bd709..696cf2d 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -1,5 +1,6 @@ require 'socket' require 'ib/support' +require 'ib/prepare_data' module IB # includes methods from IB:.Support # which adds a tws-method to @@ -9,62 +10,12 @@ module IB # - 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") - 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 + 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 @@ -113,7 +64,7 @@ def send_messages *data def receive_messages begin complete_message_buffer = [] - begin + begin # this is the blocking version of recv buffer = self.recvfrom(4096)[0] # STDOUT.puts "BUFFER:: #{buffer.inspect}" @@ -130,6 +81,6 @@ def receive_messages end end -end # class IBSocket + end # class Socket end # module IB From b77649112487daedc3a8842823eda1c0985f905c Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 4 Apr 2024 20:33:47 +0200 Subject: [PATCH 18/76] Apply Zeitwerk --- VERSION | 2 +- api.gemspec | 7 + bin/console | 20 +- .../ib/execution_condition.rb | 102 -------- conditions/ib/margin_condition.rb | 115 +++++++++ conditions/ib/order_condition.rb | 30 +++ conditions/ib/percent_change_condition.rb | 37 +++ conditions/ib/price_condition.rb | 44 ++++ conditions/ib/time_condition.rb | 42 ++++ conditions/ib/volume_condition.rb | 40 +++ ...lass-extensions.rb => class_extensions.rb} | 28 +++ lib/ib-api.rb | 42 +++- lib/ib/base_properties.rb | 6 +- lib/ib/connection.rb | 29 +-- lib/ib/constants.rb | 8 +- lib/ib/contract.rb | 30 +++ lib/ib/messages.rb | 48 +++- lib/ib/messages/abstract_message.rb | 37 --- lib/ib/messages/incoming.rb | 110 ++++++--- lib/ib/messages/incoming/abstract_message.rb | 4 +- lib/ib/messages/incoming/abstract_tick.rb | 25 ++ lib/ib/messages/incoming/account_message.rb | 26 ++ lib/ib/messages/incoming/account_value.rb | 82 ------- lib/ib/messages/incoming/contract_data.rb | 11 +- lib/ib/messages/incoming/contract_message.rb | 13 + lib/ib/messages/incoming/histogram_data.rb | 30 +++ lib/ib/messages/incoming/historical_data.rb | 69 +----- .../incoming/historical_data_update.rb | 50 ++++ lib/ib/messages/incoming/managed_accounts.rb | 21 ++ .../{market_depths.rb => market_depth.rb} | 10 - lib/ib/messages/incoming/market_depth_l2.rb | 15 ++ lib/ib/messages/incoming/next_valid_id.rb | 1 + lib/ib/messages/incoming/portfolio_value.rb | 41 +--- lib/ib/messages/incoming/position_data.rb | 21 ++ lib/ib/messages/incoming/positions_multi.rb | 15 ++ lib/ib/messages/incoming/receive_fa.rb | 31 +++ lib/ib/messages/incoming/tick_by_tick.rb | 77 ++++++ lib/ib/messages/incoming/tick_efp.rb | 18 ++ lib/ib/messages/incoming/tick_generic.rb | 13 + lib/ib/messages/incoming/tick_option.rb | 60 +++++ lib/ib/messages/incoming/tick_price.rb | 60 +++++ lib/ib/messages/incoming/tick_size.rb | 55 +++++ lib/ib/messages/incoming/tick_string.rb | 13 + lib/ib/messages/incoming/ticks.rb | 229 ------------------ lib/ib/messages/outgoing.rb | 141 ++++------- lib/ib/messages/outgoing/abstract_message.rb | 2 +- ...bar_requests.rb => bar_request_message.rb} | 1 + lib/ib/messages/outgoing/place_order.rb | 1 + ...requests.rb => request_account_summary.rb} | 36 +-- .../outgoing/request_historical_data.rb | 182 ++++++++++++++ ...t_marketdata.rb => request_market_data.rb} | 21 +- .../messages/outgoing/request_market_depth.rb | 1 - .../outgoing/request_real_time_bars.rb | 48 ++++ .../outgoing/request_scanner_subscription.rb | 73 ++++++ ...k_data.rb => request_tick_by_tick_data.rb} | 0 lib/ib/model.rb | 4 - lib/ib/models.rb | 14 -- lib/ib/order_condition.rb | 26 ++ lib/ib/prepare_data.rb | 22 +- lib/ib/socket.rb | 11 +- lib/ib/version.rb | 2 +- lib/models/ib/vertical.rb | 96 -------- lib/requires.rb | 20 -- lib/{ib => }/server_versions.rb | 0 lib/{ => support}/logging.rb | 4 +- {lib/models => models}/ib/account.rb | 2 +- {lib/models => models}/ib/account_value.rb | 2 +- {lib/models => models}/ib/bag.rb | 8 - {lib/models => models}/ib/bar.rb | 2 +- {lib/models => models}/ib/combo_leg.rb | 2 +- {lib/models => models}/ib/contract.rb | 66 ++--- {lib/models => models}/ib/contract_detail.rb | 2 +- {lib/models => models}/ib/execution.rb | 2 +- {lib/models => models}/ib/forex.rb | 1 - {lib/models => models}/ib/future.rb | 1 - {lib/models => models}/ib/index.rb | 1 - {lib/models => models}/ib/option.rb | 3 - {lib/models => models}/ib/option_detail.rb | 2 +- {lib/models => models}/ib/order.rb | 4 +- {lib/models => models}/ib/order_state.rb | 2 +- {lib/models => models}/ib/portfolio_value.rb | 2 +- {lib/models => models}/ib/spread.rb | 6 - {lib/models => models}/ib/stock.rb | 1 - {lib/models => models}/ib/underlying.rb | 9 +- spec/ib/connection_spec.rb | 16 ++ spec/ib/integration/fundamental_data_spec.rb | 4 +- .../messages/incoming/contract_data_spec.rb | 8 +- .../incoming/managed_accounts_spec.rb | 6 +- .../ib/messages/incoming/option_chain_spec.rb | 3 +- .../messages/incoming/position_data_spec.rb | 4 +- .../messages/incoming/positios_multi_spec.rb | 10 +- spec/ib/messages/incoming/receive_fa_spec.rb | 6 +- .../recieve_multi_account_update_spec.rb | 6 +- spec/spec.yml | 2 +- 94 files changed, 1600 insertions(+), 1055 deletions(-) rename lib/models/ib/condition.rb => conditions/ib/execution_condition.rb (57%) create mode 100644 conditions/ib/margin_condition.rb create mode 100644 conditions/ib/order_condition.rb create mode 100644 conditions/ib/percent_change_condition.rb create mode 100644 conditions/ib/price_condition.rb create mode 100644 conditions/ib/time_condition.rb create mode 100644 conditions/ib/volume_condition.rb rename lib/{extensions/class-extensions.rb => class_extensions.rb} (80%) create mode 100644 lib/ib/contract.rb create mode 100644 lib/ib/messages/incoming/abstract_tick.rb create mode 100644 lib/ib/messages/incoming/account_message.rb delete mode 100644 lib/ib/messages/incoming/account_value.rb create mode 100644 lib/ib/messages/incoming/contract_message.rb create mode 100644 lib/ib/messages/incoming/histogram_data.rb create mode 100644 lib/ib/messages/incoming/historical_data_update.rb create mode 100644 lib/ib/messages/incoming/managed_accounts.rb rename lib/ib/messages/incoming/{market_depths.rb => market_depth.rb} (73%) create mode 100644 lib/ib/messages/incoming/market_depth_l2.rb create mode 100644 lib/ib/messages/incoming/position_data.rb create mode 100644 lib/ib/messages/incoming/positions_multi.rb create mode 100644 lib/ib/messages/incoming/receive_fa.rb create mode 100644 lib/ib/messages/incoming/tick_by_tick.rb create mode 100644 lib/ib/messages/incoming/tick_efp.rb create mode 100644 lib/ib/messages/incoming/tick_generic.rb create mode 100644 lib/ib/messages/incoming/tick_option.rb create mode 100644 lib/ib/messages/incoming/tick_price.rb create mode 100644 lib/ib/messages/incoming/tick_size.rb create mode 100644 lib/ib/messages/incoming/tick_string.rb delete mode 100644 lib/ib/messages/incoming/ticks.rb rename lib/ib/messages/outgoing/{bar_requests.rb => bar_request_message.rb} (99%) rename lib/ib/messages/outgoing/{account_requests.rb => request_account_summary.rb} (63%) create mode 100644 lib/ib/messages/outgoing/request_historical_data.rb rename lib/ib/messages/outgoing/{request_marketdata.rb => request_market_data.rb} (99%) create mode 100644 lib/ib/messages/outgoing/request_real_time_bars.rb create mode 100644 lib/ib/messages/outgoing/request_scanner_subscription.rb rename lib/ib/messages/outgoing/{request_tick_data.rb => request_tick_by_tick_data.rb} (100%) delete mode 100644 lib/ib/model.rb delete mode 100644 lib/ib/models.rb create mode 100644 lib/ib/order_condition.rb delete mode 100644 lib/models/ib/vertical.rb delete mode 100644 lib/requires.rb rename lib/{ib => }/server_versions.rb (100%) rename lib/{ => support}/logging.rb (91%) rename {lib/models => models}/ib/account.rb (98%) rename {lib/models => models}/ib/account_value.rb (94%) rename {lib/models => models}/ib/bag.rb (92%) rename {lib/models => models}/ib/bar.rb (98%) rename {lib/models => models}/ib/combo_leg.rb (99%) rename {lib/models => models}/ib/contract.rb (93%) rename {lib/models => models}/ib/contract_detail.rb (99%) rename {lib/models => models}/ib/execution.rb (98%) rename {lib/models => models}/ib/forex.rb (90%) rename {lib/models => models}/ib/future.rb (91%) rename {lib/models => models}/ib/index.rb (91%) rename {lib/models => models}/ib/option.rb (97%) rename {lib/models => models}/ib/option_detail.rb (98%) rename {lib/models => models}/ib/order.rb (99%) rename {lib/models => models}/ib/order_state.rb (99%) rename {lib/models => models}/ib/portfolio_value.rb (98%) rename {lib/models => models}/ib/spread.rb (97%) rename {lib/models => models}/ib/stock.rb (95%) rename {lib/models => models}/ib/underlying.rb (84%) create mode 100644 spec/ib/connection_spec.rb 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..ab34d7e 100644 --- a/api.gemspec +++ b/api.gemspec @@ -42,4 +42,11 @@ 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 '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..53a934c 100755 --- a/bin/console +++ b/bin/console @@ -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 @@ -32,16 +32,16 @@ class Array end # Array -# read items from console.yml +# 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-Core Interactive Console <<" puts '-'* 45 - puts + puts puts "Namespace is IB ! " puts puts '-'* 45 @@ -59,10 +59,10 @@ read_yml = -> (key) do 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 + ## 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 } } @@ -70,12 +70,12 @@ read_yml = -> (key) do 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 "Detected Accounts: #{msg.accounts.account.join(' -- ')} " # puts # end c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } - end + end #C.logger.level = Logger::FATAL unless C.received[:OpenOrder].blank? puts "------------------------------- OpenOrders ----------------------------------" diff --git a/lib/models/ib/condition.rb b/conditions/ib/execution_condition.rb similarity index 57% rename from lib/models/ib/condition.rb rename to conditions/ib/execution_condition.rb index fa68c08..e3bc542 100644 --- a/lib/models/ib/condition.rb +++ b/conditions/ib/execution_condition.rb @@ -1,108 +1,6 @@ -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 IB::Support # 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 IB::Support # 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 IB::Support # refine Array-method for decoding of IB-Messages diff --git a/conditions/ib/margin_condition.rb b/conditions/ib/margin_condition.rb new file mode 100644 index 0000000..81f793a --- /dev/null +++ b/conditions/ib/margin_condition.rb @@ -0,0 +1,115 @@ +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 + + + class VolumeCondition < OrderCondition + using IB::Support # 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 IB::Support # 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 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/conditions/ib/order_condition.rb b/conditions/ib/order_condition.rb new file mode 100644 index 0000000..190af74 --- /dev/null +++ b/conditions/ib/order_condition.rb @@ -0,0 +1,30 @@ +module IB + class OrderCondition < IB::Base + 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 + + + +end # module diff --git a/conditions/ib/percent_change_condition.rb b/conditions/ib/percent_change_condition.rb new file mode 100644 index 0000000..7960a23 --- /dev/null +++ b/conditions/ib/percent_change_condition.rb @@ -0,0 +1,37 @@ +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..a2dfce0 --- /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..99c2422 --- /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..6b750ad --- /dev/null +++ b/conditions/ib/volume_condition.rb @@ -0,0 +1,40 @@ +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/extensions/class-extensions.rb b/lib/class_extensions.rb similarity index 80% rename from lib/extensions/class-extensions.rb rename to lib/class_extensions.rb index 26ba2a4..6203765 100644 --- a/lib/extensions/class-extensions.rb +++ b/lib/class_extensions.rb @@ -36,6 +36,11 @@ def to_bool self == 0 ? false : true end end + module Extensions + def blank? + false + end + end end module BoolClass @@ -45,8 +50,19 @@ 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 @@ -66,6 +82,12 @@ def to_f 0 end end + module Extensions + def blank? + false + end + end + module Sort # ActiveModel serialization depends on this method def <=> other @@ -85,14 +107,20 @@ def to_sup Array.include ClassExtensions::Array::DuplicatesCounter Array.include ClassExtensions::Array::TablePresenter FalseClass.include ClassExtensions::BoolClass::Bool +FalseClass.include ClassExtensions::BoolClass::Extensions 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 diff --git a/lib/ib-api.rb b/lib/ib-api.rb index d0c2116..8afdbf0 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -1,7 +1,39 @@ -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 'class_extensions' +require 'logger' +require 'terminal-table' -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 +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__}/models") +loader.inflector.inflect( + "ib" => "IB", + "receive_fa" => "ReceiveFA", + "tick_efp" => "TickEFP", + ) +#loader.push_dir("#{__dir__}") +loader.push_dir("#{__dir__}/../models/") +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_properties.rb b/lib/ib/base_properties.rb index 2fc1a5f..5d9ba0a 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 @@ -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 d55b27f..423996c 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -1,9 +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 @@ -109,7 +103,7 @@ def update_next_order_id ### Working with connection # - ### connect can be called directly. but is mostly called through update_next_order_id + ### connect can be called directly, but is mostly addressed through update_next_order_id def connect logger.progname='IB::Connection#connect' if connected? @@ -120,12 +114,12 @@ def connect self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible socket.initialising_handshake socket.decode_message( socket.receive_messages ) do | the_message | - # logger.info{ "TheMessage :: #{the_message.inspect}" } - @server_version = the_message.shift.to_i + #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 - @local_connect_time = Time.now + @remote_connect_time = DateTime.parse the_message.shift.freeze + @local_connect_time = Time.now.freeze end # V100 initial handshake @@ -175,17 +169,18 @@ def subscribe *args, &block error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc args.each do |what| + puts "What: #{what.inspect}" 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 } diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index 8a841ee..8296319 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -1,4 +1,5 @@ module IB + ### Widely used TWS constants: EOL = "\0" @@ -227,12 +228,15 @@ module IB 'STK' => :stock, 'WAR' => :warrant, 'ICU' => :icu, - 'ICS' => :ics, + 'ICS' => :ics, 'BILL' => :bill, 'BSK' => :basket, 'FWD' => :forward, 'FIXED' => :fixed , - 'CRYPTO' => :crypto }.freeze + '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 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/messages.rb b/lib/ib/messages.rb index 10ed272..f4fc854 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: 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 diff --git a/lib/ib/messages/abstract_message.rb b/lib/ib/messages/abstract_message.rb index 74423f5..eb40897 100644 --- a/lib/ib/messages/abstract_message.rb +++ b/lib/ib/messages/abstract_message.rb @@ -60,42 +60,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 39bcf96..9ea541a 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). @@ -33,13 +33,13 @@ module Incoming ScannerParameters = def_message 19, [:xml, :xml] class ScannerParameters - # returns a List of Hashes specifing Instruments. + # 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", + # => {: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] @@ -47,16 +47,16 @@ def instruments # 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"} -# + # => {: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] @@ -98,7 +98,7 @@ def scan_types [:yield, :decimal_max], [:yield_redemption_date, :int] # YYYYMMDD format - SecurityDefinitionOptionParameter = OptionChainDefinition = def_message [75,0] , + SecurityDefinitionOptionParameter = OptionChainDefinition = def_message [75,0] , [:request_id, :int], [:exchange, :string], [:con_id, :int], # underlying_con_id @@ -109,7 +109,7 @@ class OptionChainDefinition using IB::Support # defines tws-method for Array (socket.rb) def load super - load_map [:expirations, :array, proc { @buffer.read_date }], + load_map [:expirations, :array, proc { @buffer.read_date }], [:strikes, :array, proc { @buffer.read_decimal } ] end def expirations @@ -123,8 +123,6 @@ 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 ] @@ -133,36 +131,72 @@ def to_human #<- 1-9-789--USD-CASH-----IDEALPRO--CAD------ #-> ---81-123-5.0E-5--0- - MarketDepthExchanges = def_message [80,0], + 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 +# 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 , :decimal], + [ :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 diff --git a/lib/ib/messages/incoming/abstract_message.rb b/lib/ib/messages/incoming/abstract_message.rb index 295f1d3..0f29213 100644 --- a/lib/ib/messages/incoming/abstract_message.rb +++ b/lib/ib/messages/incoming/abstract_message.rb @@ -1,5 +1,5 @@ -require 'ib/messages/abstract_message' -require 'ib/support' +#require 'ib/messages/abstract_message' +#require 'ib/support' require 'ox' module IB module Messages diff --git a/lib/ib/messages/incoming/abstract_tick.rb b/lib/ib/messages/incoming/abstract_tick.rb new file mode 100644 index 0000000..885ff98 --- /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..ceedf7b --- /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 - "" + 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 7c95f1e..6cc8385 100644 --- a/lib/ib/messages/incoming/historical_data.rb +++ b/lib/ib/messages/incoming/historical_data.rb @@ -11,7 +11,7 @@ module Incoming # - 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: # - date - The date-time stamp of the start of the bar. The format is set to sec since EPOCHE # in outgoing/bar_requests ReqHistoricalData. @@ -33,7 +33,7 @@ module Incoming class HistoricalData attr_accessor :results using IB::Support # extended Array-Class from abstract_message - + def load super @@ -46,9 +46,9 @@ def load :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,68 +58,7 @@ 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 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 - - 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 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..5384b54 --- /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..046a591 --- /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..1ee8e59 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/portfolio_value.rb b/lib/ib/messages/incoming/portfolio_value.rb index e255402..4e0d699 100644 --- a/lib/ib/messages/incoming/portfolio_value.rb +++ b/lib/ib/messages/incoming/portfolio_value.rb @@ -2,21 +2,16 @@ 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], + [: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] + [:account, :string] class PortfolioValue @@ -26,10 +21,9 @@ 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 = IB::PortfolioValue.new @data[:portfolio_value] @portfolio_value.contract = contract @portfolio_value.account = account end @@ -40,36 +34,11 @@ def account_name @account_name = @data[:account] end -# alias :to_human :portfolio_value +# 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..586c185 --- /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..1df7d4a --- /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..69df5ea --- /dev/null +++ b/lib/ib/messages/incoming/receive_fa.rb @@ -0,0 +1,31 @@ + +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/tick_by_tick.rb b/lib/ib/messages/incoming/tick_by_tick.rb new file mode 100644 index 0000000..95e7189 --- /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..3743340 --- /dev/null +++ b/lib/ib/messages/incoming/tick_generic.rb @@ -0,0 +1,13 @@ + +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..fa7e1b2 --- /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..bc38158 --- /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, :float], + [: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 641c61a..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, 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 - - 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 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 # module Incoming - end # module Messages -end # module IB diff --git a/lib/ib/messages/outgoing.rb b/lib/ib/messages/outgoing.rb index 2d35f15..faa1fba 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 @@ -124,7 +124,6 @@ module Outgoing [:exchange, ""], # futOptExchange :sec_type, # underlyingSecType :con_id # underlyingConId (required) - # data = { :id => ticker_id (int), :contract => Contract, :num_rows => int } @@ -132,17 +131,17 @@ module Outgoing # returns MarketDepthExchanges-Message # RequestMarketDepthExchanges = # requires ServerVersion >= 112 - def_message 82 + def_message 82 - - ## actual Version supported is: 137 + + ## 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_supershort ], - :num_rows, + :num_rows, "") # mktDataOptionsStr. ## not supported by api # When this message is sent, TWS responds with ExecutionData messages, each @@ -181,7 +180,7 @@ module Outgoing # exercised. Values are: # - 0 = do not override # - 1 = override - ExerciseOptions = def_message([ 21, 2 ], + ExerciseOptions = def_message([ 21, 2 ], # :request_id, # id -> required # todo : TEST [:contract, :serialize_short], :exercise_action, @@ -216,7 +215,7 @@ module Outgoing #ReportSnapshot Company's financial overview #ReportsFinStatements Financial Statements #RESC Analyst Estimates - #CalendarReport Company's calendar + #CalendarReport Company's calendar RequestFundamentalData = def_message([52,2], :request_id, # autogenerated if not specified @@ -224,12 +223,12 @@ module Outgoing :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 + # 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 + RequestHeadTimeStamp = + def_message( [87,0], :request_id, # autogenerated [:contract, :serialize_short, [:primary_exchange,:include_expired] ], [:use_rth, 1 ], [:what_to_show, 'Trades' ], @@ -240,7 +239,7 @@ module Outgoing - RequestHistogramData = + RequestHistogramData = def_message( [88, 0], :request_id, # autogenerated [:contract, :serialize_short, [:primary_exchange,:include_expired] ], [:use_rth, 1 ], @@ -270,81 +269,45 @@ module Outgoing [: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_market_depth' - require 'ib/messages/outgoing/request_tick_data' + + + 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 diff --git a/lib/ib/messages/outgoing/abstract_message.rb b/lib/ib/messages/outgoing/abstract_message.rb index 95a981f..c91ecd5 100644 --- a/lib/ib/messages/outgoing/abstract_message.rb +++ b/lib/ib/messages/outgoing/abstract_message.rb @@ -1,4 +1,4 @@ -require 'ib/messages/abstract_message' +#require 'ib/messages/abstract_message' module IB module Messages diff --git a/lib/ib/messages/outgoing/bar_requests.rb b/lib/ib/messages/outgoing/bar_request_message.rb similarity index 99% rename from lib/ib/messages/outgoing/bar_requests.rb rename to lib/ib/messages/outgoing/bar_request_message.rb index 19b36fa..1506eca 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 diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 9b67c92..c0f4d60 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -1,6 +1,7 @@ module IB module Messages module Outgoing + extend Messages # def_message macros # Data format is { :id => int: local_id, # :contract => Contract, diff --git a/lib/ib/messages/outgoing/account_requests.rb b/lib/ib/messages/outgoing/request_account_summary.rb similarity index 63% rename from lib/ib/messages/outgoing/account_requests.rb rename to lib/ib/messages/outgoing/request_account_summary.rb index cc637a7..9574adb 100644 --- a/lib/ib/messages/outgoing/account_requests.rb +++ b/lib/ib/messages/outgoing/request_account_summary.rb @@ -2,14 +2,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 @@ -71,36 +65,10 @@ module Outgoing [: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 - - - # 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 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..dbf8294 --- /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 99% rename from lib/ib/messages/outgoing/request_marketdata.rb rename to lib/ib/messages/outgoing/request_market_data.rb index 758874c..c1ddc74 100644 --- a/lib/ib/messages/outgoing/request_marketdata.rb +++ b/lib/ib/messages/outgoing/request_market_data.rb @@ -4,6 +4,17 @@ module Messages module Outgoing extend Messages # def_message macros + 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, ""] # 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 @@ -88,15 +99,5 @@ module Outgoing # :mktDataOptions => (TagValueList) For internal use only. # 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, ""] # changed to enable requests in V 10.19 ff - end end end diff --git a/lib/ib/messages/outgoing/request_market_depth.rb b/lib/ib/messages/outgoing/request_market_depth.rb index 9c821ce..dc58e83 100644 --- a/lib/ib/messages/outgoing/request_market_depth.rb +++ b/lib/ib/messages/outgoing/request_market_depth.rb @@ -9,7 +9,6 @@ module Outgoing ## then: 'is_smart_depth' (bool) has to be specified in CancelMarketDepth, too # # - CancelMarketDepth = def_message([11, 1], :is_smart_depth) RequestMarketDepth = def_message( [10, 5], 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..743ab04 --- /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_data.rb b/lib/ib/messages/outgoing/request_tick_by_tick_data.rb similarity index 100% rename from lib/ib/messages/outgoing/request_tick_data.rb rename to lib/ib/messages/outgoing/request_tick_by_tick_data.rb 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..0f732f5 --- /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/prepare_data.rb b/lib/ib/prepare_data.rb index a0e81cf..c4c9912 100644 --- a/lib/ib/prepare_data.rb +++ b/lib/ib/prepare_data.rb @@ -35,16 +35,18 @@ def prepare_message data # # 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 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 diff --git a/lib/ib/socket.rb b/lib/ib/socket.rb index 696cf2d..e3c8753 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -1,6 +1,3 @@ -require 'socket' -require 'ib/support' -require 'ib/prepare_data' module IB # includes methods from IB:.Support # which adds a tws-method to @@ -57,7 +54,7 @@ def send_messages *data rescue Errno::ECONNRESET => e Connection.logger.fatal{ "Data not accepted by IB \n #{data.inspect} \n - Backtrace:\n "} + Backtrace:\n "} Connection.logger.error e.backtrace end @@ -67,7 +64,7 @@ def receive_messages begin # this is the blocking version of recv buffer = self.recvfrom(4096)[0] - # STDOUT.puts "BUFFER:: #{buffer.inspect}" +# STDOUT.puts "BUFFER:: #{buffer.inspect}" complete_message_buffer << buffer end while buffer.size == 4096 @@ -75,8 +72,8 @@ def receive_messages rescue Errno::ECONNRESET => e Connection.logger.fatal{ "Data Buffer is not filling \n The Buffer: #{buffer.inspect} \n - Backtrace:\n - #{e.backtrace.join("\n") } " } + Backtrace:\n + #{e.backtrace.join("\n") } " } Kernel.exit 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/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 36d3c76..0000000 --- a/lib/requires.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'active_support/core_ext/module/attribute_accessors.rb' -require_relative '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 100% rename from lib/ib/server_versions.rb rename to lib/server_versions.rb diff --git a/lib/logging.rb b/lib/support/logging.rb similarity index 91% rename from lib/logging.rb rename to lib/support/logging.rb index 09aecd3..ce7f434 100644 --- a/lib/logging.rb +++ b/lib/support/logging.rb @@ -30,8 +30,8 @@ def configure_logger(log=nil) if log @logger = log else - @logger = Logger.new(STDOUT) - @logger.level = Logger::INFO + @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" diff --git a/lib/models/ib/account.rb b/models/ib/account.rb similarity index 98% rename from lib/models/ib/account.rb rename to models/ib/account.rb index a8bfb3c..075f5a4 100644 --- a/lib/models/ib/account.rb +++ b/models/ib/account.rb @@ -1,5 +1,5 @@ module IB - class Account < IB::Model + class Account < IB::Base include BaseProperties # attr_accessible :alias, :account, :connected diff --git a/lib/models/ib/account_value.rb b/models/ib/account_value.rb similarity index 94% rename from lib/models/ib/account_value.rb rename to models/ib/account_value.rb index f3c93f1..f5a63ee 100644 --- a/lib/models/ib/account_value.rb +++ b/models/ib/account_value.rb @@ -1,6 +1,6 @@ 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 diff --git a/lib/models/ib/bag.rb b/models/ib/bag.rb similarity index 92% rename from lib/models/ib/bag.rb rename to models/ib/bag.rb index 8e54caa..807d998 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. 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 99% rename from lib/models/ib/combo_leg.rb rename to models/ib/combo_leg.rb index 390fefa..8b941b7 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 diff --git a/lib/models/ib/contract.rb b/models/ib/contract.rb similarity index 93% rename from lib/models/ib/contract.rb rename to models/ib/contract.rb index 2f3fe7c..c19e040 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 @@ -444,48 +432,22 @@ def table_row ### 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' +# begin + +# require '../models/ib/option.rb' +# require '../models/ib/bag.rb' +# require '../models/ib/forex.rb' +# require '../models/ib/future.rb' +# require '../models/ib/stock.rb' +# require '../models/ib/index.rb' ### walkaraound to enable spreads with orientdb - if IB::const_defined? :Spread - IB::send(:remove_const, :Spread) +# 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 IB::Support +# end + # require 'models/ib/spread.rb' +#^ end - 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 99% rename from lib/models/ib/contract_detail.rb rename to models/ib/contract_detail.rb index 5d8ad76..0718c24 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: 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 90% rename from lib/models/ib/forex.rb rename to models/ib/forex.rb index def7080..0999d55 100644 --- a/lib/models/ib/forex.rb +++ b/models/ib/forex.rb @@ -1,4 +1,3 @@ -#require 'models/ib/contract' module IB class Forex < IB::Contract validates_format_of :sec_type, :with => /\Aforex\z/, diff --git a/lib/models/ib/future.rb b/models/ib/future.rb similarity index 91% rename from lib/models/ib/future.rb rename to models/ib/future.rb index cf025fd..1153c56 100644 --- a/lib/models/ib/future.rb +++ b/models/ib/future.rb @@ -1,4 +1,3 @@ -#require 'models/ib/contract' module IB class Future < Contract validates_format_of :sec_type, :with => /\Afuture\z/, 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/lib/models/ib/option.rb b/models/ib/option.rb similarity index 97% rename from lib/models/ib/option.rb rename to models/ib/option.rb index 607bb72..a28c920 100644 --- a/lib/models/ib/option.rb +++ b/models/ib/option.rb @@ -1,6 +1,3 @@ -#require_relative 'contract' -require_relative 'option_detail' - module IB class Option < Contract diff --git a/lib/models/ib/option_detail.rb b/models/ib/option_detail.rb similarity index 98% rename from lib/models/ib/option_detail.rb rename to models/ib/option_detail.rb index 74cb950..fa97a8d 100644 --- a/lib/models/ib/option_detail.rb +++ b/models/ib/option_detail.rb @@ -1,7 +1,7 @@ module IB # Additional Option properties and Option-Calculations - class OptionDetail < IB::Model + class OptionDetail < IB::Base include BaseProperties prop :delta, :gamma, :vega, :theta, # greeks diff --git a/lib/models/ib/order.rb b/models/ib/order.rb similarity index 99% rename from lib/models/ib/order.rb rename to models/ib/order.rb index df85f3e..f372847 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: diff --git a/lib/models/ib/order_state.rb b/models/ib/order_state.rb similarity index 99% rename from lib/models/ib/order_state.rb rename to models/ib/order_state.rb index cb9fadc..bd56a43 100644 --- a/lib/models/ib/order_state.rb +++ b/models/ib/order_state.rb @@ -2,7 +2,7 @@ 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 diff --git a/lib/models/ib/portfolio_value.rb b/models/ib/portfolio_value.rb similarity index 98% rename from lib/models/ib/portfolio_value.rb rename to models/ib/portfolio_value.rb index d967c19..3c2a85e 100644 --- a/lib/models/ib/portfolio_value.rb +++ b/models/ib/portfolio_value.rb @@ -1,5 +1,5 @@ module IB -class PortfolioValue < IB::Model +class PortfolioValue < IB::Base include BaseProperties # belongs_to :currency belongs_to :account diff --git a/lib/models/ib/spread.rb b/models/ib/spread.rb similarity index 97% rename from lib/models/ib/spread.rb rename to models/ib/spread.rb index 877e5f8..f667967 100644 --- a/lib/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -1,10 +1,4 @@ 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 diff --git a/lib/models/ib/stock.rb b/models/ib/stock.rb similarity index 95% rename from lib/models/ib/stock.rb rename to models/ib/stock.rb index e03a878..fc43cd8 100644 --- a/lib/models/ib/stock.rb +++ b/models/ib/stock.rb @@ -1,4 +1,3 @@ -#require_relative 'contract' module IB class Stock < IB::Contract validates_format_of :sec_type, :with => /\Astock\z/, diff --git a/lib/models/ib/underlying.rb b/models/ib/underlying.rb similarity index 84% rename from lib/models/ib/underlying.rb rename to models/ib/underlying.rb index ec26885..feedd5a 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 @@ -33,5 +29,4 @@ def == other end # class Underlying UnderComp = Underlying - end -end # module IB +end diff --git a/spec/ib/connection_spec.rb b/spec/ib/connection_spec.rb new file mode 100644 index 0000000..dd6542c --- /dev/null +++ b/spec/ib/connection_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" + +describe IB::Connection do + Given( :ib ) { IB::Connection.new connect: false } + 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 diff --git a/spec/ib/integration/fundamental_data_spec.rb b/spec/ib/integration/fundamental_data_spec.rb index f475206..c2c1f74 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/contract_data_spec.rb b/spec/ib/messages/incoming/contract_data_spec.rb index 6a3e336..42a5aa2 100644 --- a/spec/ib/messages/incoming/contract_data_spec.rb +++ b/spec/ib/messages/incoming/contract_data_spec.rb @@ -14,16 +14,16 @@ 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.wait_for :ContractDetailsEnd, :ContractDataEnd end - after(:all){ IB::Connection.current.clear_received :ContractData } + 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[:ContractData].contract.last } + subject{ IB::Connection.current.received[:ContractDetails].contract.last } it_behaves_like 'a complete Contract Object' its( :sec_type ){is_expected.to eq :stock} @@ -32,7 +32,7 @@ end context "received a single contract" do - subject{ IB::Connection.current.received[:ContractData] } + subject{ IB::Connection.current.received[:ContractDetails] } it{ is_expected.to be_a Array } its(:size){is_expected.to eq 1 } end diff --git a/spec/ib/messages/incoming/managed_accounts_spec.rb b/spec/ib/messages/incoming/managed_accounts_spec.rb index 89a6858..85c1a3c 100644 --- a/spec/ib/messages/incoming/managed_accounts_spec.rb +++ b/spec/ib/messages/incoming/managed_accounts_spec.rb @@ -8,7 +8,7 @@ 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 @@ -25,11 +25,11 @@ 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 } + 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/option_chain_spec.rb b/spec/ib/messages/incoming/option_chain_spec.rb index 06dd5d4..9330218 100644 --- a/spec/ib/messages/incoming/option_chain_spec.rb +++ b/spec/ib/messages/incoming/option_chain_spec.rb @@ -26,9 +26,8 @@ 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', sec_type: "STK" #contract.sec_type diff --git a/spec/ib/messages/incoming/position_data_spec.rb b/spec/ib/messages/incoming/position_data_spec.rb index e038464..904e104 100644 --- a/spec/ib/messages/incoming/position_data_spec.rb +++ b/spec/ib/messages/incoming/position_data_spec.rb @@ -22,7 +22,7 @@ establish_connection ib = IB::Connection.current ib.send_message :RequestPositions - + ib.wait_for :PositionData sleep 1 ib.send_message :CancelPositions @@ -32,7 +32,7 @@ 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..108e259 100644 --- a/spec/ib/messages/incoming/positios_multi_spec.rb +++ b/spec/ib/messages/incoming/positios_multi_spec.rb @@ -1,6 +1,6 @@ require 'main_helper' -RSpec.shared_examples 'Position Message' do +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 } @@ -17,7 +17,7 @@ RSpec.describe IB::Messages::Incoming::PositionsMulti do context "Syntetic Message" do - let( :the_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 @@ -27,7 +27,7 @@ expect( the_message.contract.symbol ).to eq 'LHA' puts the_message.inspect end - it_behaves_like 'Position Message' + it_behaves_like 'Position Message' end context 'Message received from IB', :connected => true do @@ -41,9 +41,9 @@ end after(:all) { close_connection } - + it_behaves_like 'Position Message' do - let( :the_message ){ IB::Connection.current.received[:PositionsMulti].first } + let( :the_message ){ IB::Connection.current.received[:PositionsMulti].first } end diff --git a/spec/ib/messages/incoming/receive_fa_spec.rb b/spec/ib/messages/incoming/receive_fa_spec.rb index 3cef9ad..eefbc7e 100644 --- a/spec/ib/messages/incoming/receive_fa_spec.rb +++ b/spec/ib/messages/incoming/receive_fa_spec.rb @@ -9,7 +9,7 @@ before(:all) do establish_connection ib = IB::Connection.current - + ib.send_message :RequestFA, fa_data_type: 3 # alias ib.wait_for :ReceiveFA @@ -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 } + 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..eefbc7e 100644 --- a/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb +++ b/spec/ib/messages/incoming/recieve_multi_account_update_spec.rb @@ -9,7 +9,7 @@ before(:all) do establish_connection ib = IB::Connection.current - + ib.send_message :RequestFA, fa_data_type: 3 # alias ib.wait_for :ReceiveFA @@ -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 } + let( :the_account_object ){ IB::Connection.current.received[:ReceiveFA].first.accounts.first } end end # end # describe IB::Messages:Incoming diff --git a/spec/spec.yml b/spec/spec.yml index 91abd12..7917c5a 100644 --- a/spec/spec.yml +++ b/spec/spec.yml @@ -7,7 +7,7 @@ :reuters: false # currently not used :account: 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 From e35d67c3070fc8dd57702701072ed2fe928f322d Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 4 Apr 2024 20:48:49 +0200 Subject: [PATCH 19/76] completed zeitwerk integration --- changelog.md | 6 ++++++ lib/ib-api.rb | 2 +- lib/ib/connection.rb | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index e30c643..6104030 100644 --- a/changelog.md +++ b/changelog.md @@ -17,3 +17,9 @@ 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 diff --git a/lib/ib-api.rb b/lib/ib-api.rb index 8afdbf0..3c25ca1 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -15,7 +15,7 @@ require 'ib/constants' require 'ib/errors' #loader = Zeitwerk::Loader.new -loader = Zeitwerk::Loader.for_gem +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") diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 423996c..1ea1ab9 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -169,7 +169,6 @@ def subscribe *args, &block error "Need subscriber proc or block ", :args unless subscriber.is_a? Proc args.each do |what| - puts "What: #{what.inspect}" message_classes = case when what.is_a?(Class) && what < IB::Messages::Incoming::AbstractMessage From 53a24e7c431316d1edb64f7a6eb975457d924a83 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 11 Apr 2024 09:54:06 +0200 Subject: [PATCH 20/76] Introducing plugins to extend the functionality including basic plugins --- README.md | 35 ++- lib/ib/connection.rb | 14 + lib/ib/errors.rb | 3 + lib/ib/messages/outgoing.rb | 19 +- lib/ib/messages/outgoing/place_order.rb | 24 +- lib/ib/plugins.rb | 25 ++ lib/ib/support.rb | 6 +- models/ib/contract.rb | 96 +++---- models/ib/future.rb | 24 ++ plugins/ib/eod.rb | 282 +++++++++++++++++++ plugins/ib/greeks.rb | 93 ++++++ plugins/ib/market-price.rb | 133 +++++++++ plugins/ib/option-chain.rb | 167 +++++++++++ plugins/ib/order-prototypes.rb | 104 +++++++ plugins/ib/order_prototypes/abstract.rb | 67 +++++ plugins/ib/order_prototypes/all-in-one.rb | 46 +++ plugins/ib/order_prototypes/combo.rb | 46 +++ plugins/ib/order_prototypes/forex.rb | 40 +++ plugins/ib/order_prototypes/limit.rb | 177 ++++++++++++ plugins/ib/order_prototypes/market.rb | 116 ++++++++ plugins/ib/order_prototypes/pegged.rb | 173 ++++++++++++ plugins/ib/order_prototypes/premarket.rb | 31 ++ plugins/ib/order_prototypes/stop.rb | 202 +++++++++++++ plugins/ib/order_prototypes/volatility.rb | 39 +++ plugins/ib/probability-of-expiring.rb | 109 +++++++ plugins/ib/spread-prototypes.rb | 64 +++++ plugins/ib/spread_prototypes/butterfly.rb | 77 +++++ plugins/ib/spread_prototypes/calendar.rb | 87 ++++++ plugins/ib/spread_prototypes/stock-spread.rb | 47 ++++ plugins/ib/spread_prototypes/straddle.rb | 68 +++++ plugins/ib/spread_prototypes/strangle.rb | 95 +++++++ plugins/ib/spread_prototypes/vertical.rb | 83 ++++++ plugins/ib/verify.rb | 190 +++++++++++++ spec/ib/connection_spec.rb | 17 ++ spec/ib/plugins_spec.rb | 44 +++ spec/main_helper.rb | 4 +- spec/spec.yml | 6 +- spec/spec_helper.rb | 2 +- 38 files changed, 2761 insertions(+), 94 deletions(-) create mode 100644 lib/ib/plugins.rb create mode 100644 plugins/ib/eod.rb create mode 100644 plugins/ib/greeks.rb create mode 100644 plugins/ib/market-price.rb create mode 100644 plugins/ib/option-chain.rb create mode 100644 plugins/ib/order-prototypes.rb create mode 100644 plugins/ib/order_prototypes/abstract.rb create mode 100644 plugins/ib/order_prototypes/all-in-one.rb create mode 100644 plugins/ib/order_prototypes/combo.rb create mode 100644 plugins/ib/order_prototypes/forex.rb create mode 100644 plugins/ib/order_prototypes/limit.rb create mode 100644 plugins/ib/order_prototypes/market.rb create mode 100644 plugins/ib/order_prototypes/pegged.rb create mode 100644 plugins/ib/order_prototypes/premarket.rb create mode 100644 plugins/ib/order_prototypes/stop.rb create mode 100644 plugins/ib/order_prototypes/volatility.rb create mode 100644 plugins/ib/probability-of-expiring.rb create mode 100644 plugins/ib/spread-prototypes.rb create mode 100644 plugins/ib/spread_prototypes/butterfly.rb create mode 100644 plugins/ib/spread_prototypes/calendar.rb create mode 100644 plugins/ib/spread_prototypes/stock-spread.rb create mode 100644 plugins/ib/spread_prototypes/straddle.rb create mode 100644 plugins/ib/spread_prototypes/strangle.rb create mode 100644 plugins/ib/spread_prototypes/vertical.rb create mode 100644 plugins/ib/verify.rb create mode 100644 spec/ib/plugins_spec.rb diff --git a/README.md b/README.md index d031e93..058ff7e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Ruby interface to Interactive Brokers' TWS API Reimplementation of the basic functions of ib-ruby --- -__STATUS: Preparing for a new GEM-Release, scheduled for July__ (delayed to August) +__STATUS: Preparing for a new GEM-Release) --- @@ -23,7 +23,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,11 +52,38 @@ puts ib.recieved[:OrderStatus].to_human ``` +## Plugins + +**IB-API** ships with simple plugins to facilitate automations + +```ruby +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", } +``` + +Currently implemented plugins + +* verify: get contract details from the tws +* market-price: fetch the current market-price of a contract +* eod: retrieve EOD-Data for the given contract +* greeks: read current option greeks +* 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 + + ##### User-specific Actions Besides storing any TWS-response in an array, callbacks are implemented. The user subscribes to a certain response and defines the actions in a typically ruby manner. These actions -can be defined globaly +can be defined globally ```ruby ib = IB::Connection.new do |tws| # Subscribe to TWS alerts/errors and order-related messages @@ -65,7 +92,7 @@ ib = IB::Connection.new do |tws| ``` -or occationally +or occasionally ```ruby # first define actions diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 1ea1ab9..6238e30 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -14,6 +14,8 @@ class Connection ## misc: reader_running? include ::Support::Logging # provides default_logger + include Plugins + mattr_accessor :current # Please note, we are realizing only the most current TWS protocol versions, @@ -25,6 +27,7 @@ class Connection attr_accessor :client_id attr_accessor :server_version attr_accessor :client_version + attr_accessor :plugins alias next_order_id next_local_id alias next_order_id= next_local_id= @@ -38,12 +41,15 @@ def initialize host: '127.0.0.1', client_id: rand( 1001 .. 9999 ) , 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. 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 @@ -51,12 +57,20 @@ def initialize host: '127.0.0.1', v = eval(k.to_s) instance_variable_set("@#{k}", v) unless v.nil? end + puts "@host: #{@host}" + puts "@port: #{@port}" # A couple of locks to avoid race conditions in JRuby @subscribe_lock = Mutex.new @receive_lock = Mutex.new @message_lock = Mutex.new + @plugins.each do |name| + activate_plugin name + end + + + @connected = false self.next_local_id = nil diff --git a/lib/ib/errors.rb b/lib/ib/errors.rb index 2774d25..396c3a6 100644 --- a/lib/ib/errors.rb +++ b/lib/ib/errors.rb @@ -17,6 +17,9 @@ class FlexError < RuntimeError end class TransmissionError < RuntimeError + end + # define a custom ErrorClass which can be fired if a verification fails + class VerifyError < StandardError end end # module IB diff --git a/lib/ib/messages/outgoing.rb b/lib/ib/messages/outgoing.rb index faa1fba..b91349b 100644 --- a/lib/ib/messages/outgoing.rb +++ b/lib/ib/messages/outgoing.rb @@ -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,15 +107,15 @@ 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 + + # 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 + # 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: + # con_id: # Responses via Messages::Incoming::SecurityDefinitionOptionParameter diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index c0f4d60..d6bc4a4 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -12,7 +12,6 @@ module Outgoing ## Max-Client_ver --> 144!! class PlaceOrder - def encode # server_version = Connection.current.server_version order = @data[:order] @@ -26,7 +25,7 @@ def encode (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.limit_price, order.aux_price, order[:tif], order.oca_group, @@ -52,13 +51,11 @@ def encode ## Support for combo routing params in Order order.combo_params.empty? ? 0 : [order.combo_params.size] + order.combo_params.to_a ] - else - [] - end, - - + else + [] + end, "", # deprecated shares_allocation field - order.discretionary_amount, + order.discretionary_amount, order.good_after_time, order.good_till_date, [ order.fa_group, @@ -170,20 +167,20 @@ def encode 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.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 +# 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 +# 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 +# if server_version >= 148 # min_server_ver_d_peg_orders order.discretionary_up_to_limit_price # end ] ] @@ -199,7 +196,6 @@ def encode # if self.serverVersion() >= MIN_SERVER_VER_D_PEG_ORDERS: 148 # flds.append(make_field(order.discretionaryUpToLimitPrice)) # -# end end # PlaceOrder diff --git a/lib/ib/plugins.rb b/lib/ib/plugins.rb new file mode 100644 index 0000000..d5293f3 --- /dev/null +++ b/lib/ib/plugins.rb @@ -0,0 +1,25 @@ +module IB + module Plugins + def activate_plugin name + unless @plugins.include? name + # root= base directory of the ib-api source + root= Pathname.new( File.expand_path("../../../", __FILE__ )) + # plugins are defined in ib-api/plugins/ib + filename= root + "plugins/ib/#{name}.rb" + if filename.exist? + if require filename + @plugins << name + true # return value + else + error "Could not load Plugin `#{name}` --> #{filename} " + end + else + error "Plugin `#{name}` not found in `plugins/ib/`" + nil + end + else + IB::Connection.logger.debug "Already activated plugin #{name}" + end + end + end +end diff --git a/lib/ib/support.rb b/lib/ib/support.rb index f986b78..b44a9ec 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -28,7 +28,7 @@ def read_float end def read_decimal i= self.shift rescue nil - i = i.to_d unless i.blank? + i = BigDecimal(i) unless i.blank? i.is_a?(Numeric) && i < IB::TWS_MAX ? i : nil # return nil, if a very large number is transmitted end @@ -36,13 +36,13 @@ def read_decimal ## Values -1 and below indicate: Not computed (TickOptionComputation) def read_decimal_limit_1 - i= read_float + i= read_decimal i <= -1 ? nil : i end ## Values -2 and below indicate: Not computed (TickOptionComputation) def read_decimal_limit_2 - i= read_float + i= read_decimal i <= -2 ? nil : i end diff --git a/models/ib/contract.rb b/models/ib/contract.rb index c19e040..8cb67b5 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -115,12 +115,12 @@ def default_attributes # :nodoc: 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 def serialize *fields # :nodoc: @@ -129,10 +129,10 @@ def serialize *fields # :nodoc: print_default[symbol], print_default[self[:sec_type]], ( fields.include?(:option) ? - [ print_default[expiry], + [ 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[self[:right]], print_default[multiplier]] : nil ), print_default[exchange], ( fields.include?(:primary_exchange) ? print_default[primary_exchange] : nil ) , @@ -144,29 +144,29 @@ 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 + # 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: + def serialize_under_comp *args # :nodoc: under_comp ? under_comp.serialize : [false] end @@ -174,7 +174,7 @@ def serialize_under_comp *args # :nodoc: def serialize_legs *fields # :nodoc: case when !bag? - [] + [] when combo_legs.empty? [0] else @@ -200,21 +200,21 @@ def serialize_ib_ruby serialize_long.join(":") end - # extracts essential attributes of the contract, + # 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, + 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 + new_contract[:description] = if @description.present? + @description elsif contract_detail.present? contract_detail.long_name - else + else "" end new_contract # return contract @@ -224,7 +224,7 @@ def essential # creates a new Contract substituting attributes by the provided key-value pairs. # # for convenience - # con_id, local_symbol and last_trading_day are resetted, + # con_id, local_symbol and last_trading_day are resetted, # the link to contract-details is savaged # # Example @@ -256,7 +256,7 @@ 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] @@ -267,7 +267,7 @@ def merge **new_attributes # Contract comparison - def == other # :nodoc: + 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 @@ -312,7 +312,7 @@ def == other # :nodoc: def to_s "" end @@ -329,7 +329,7 @@ def to_human end def to_short - if expiry.blank? && last_trading_day.blank? + if expiry.blank? && last_trading_day.blank? "#{symbol}# {exchange}# {currency}" elsif expiry.present? "#{symbol}(#{strike}) #{right} #{expiry} /#{exchange}/#{currency}" @@ -369,9 +369,9 @@ def crypto? # :nodoc: end - def verify # :nodoc: - error "verify must be overloaded. Please require at least `ib/verify` from the `ib-extenstions` gem " - end +# def verify # :nodoc: +# error "verify must be overloaded. Please require at least `ib/verify` from the `ib-extenstions` gem " +# end =begin From the release notes of TWS 9.50 @@ -385,9 +385,9 @@ 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]' @@ -399,7 +399,7 @@ def expiry end end - + # is read by Account#PlaceOrder to set requirements for contract-types, as NonGuaranteed for stock-spreads def order_requirements Hash.new @@ -413,41 +413,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}, + [ 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}, + {value: right == :none ? "": right, alignment: :center }, + { value: strike.zero? ? "": strike, alignment: :right}, { value: currency, alignment: :center} ] end - - end # class Contract - - - ### Now let's deal with Contract subclasses -# begin - -# require '../models/ib/option.rb' -# require '../models/ib/bag.rb' -# require '../models/ib/forex.rb' -# require '../models/ib/future.rb' -# require '../models/ib/stock.rb' -# require '../models/ib/index.rb' - ### 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 - end # module IB diff --git a/models/ib/future.rb b/models/ib/future.rb index 1153c56..87698a2 100644 --- a/models/ib/future.rb +++ b/models/ib/future.rb @@ -9,6 +9,30 @@ def to_human "" end + class << self + # 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 time=Time.now + [3, 6, 9, 12].find { |month| month > time.month } || 3 # for December, next March + end + + def next_quarter_year time=Time.now + next_quarter_month(time) < time.month ? time.year + 1 : time.year + end + + # WARNING: This returns the next + # quarterly expiration month after the current month. Many futures + # instruments have monthly contracts for the near months. This + # method will not work for such contracts; it will return the next + # quarter after the current month, even though the present month + # has the majority of the trading volume. + # + def next_expiry time=Time.now + "#{ next_quarter_year(time) }#{ sprintf("%02d", next_quarter_month(time)) }" + end + + end end end diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb new file mode 100644 index 0000000..4e880f0 --- /dev/null +++ b/plugins/ib/eod.rb @@ -0,0 +1,282 @@ +module IB +require 'active_support/core_ext/date/calculations' +require 'csv' + + 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 + # 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: + # :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 + + barsize = case normalize_duration.call(duration)[-1].upcase + when "W" + :week1 + when "M" + :month1 + else + :day1 + end + + + get_bars(to.to_time.to_ib , 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 + + 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].include? msg.code + tws.logger.info 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 + received.close + elsif msg.code.to_i == 2174 + tws.logger.info "Please switch to the \"10-19\"-Branch of the git-repository" + 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..d67937f --- /dev/null +++ b/plugins/ib/greeks.rb @@ -0,0 +1,93 @@ +module IB + 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/market-price.rb b/plugins/ib/market-price.rb new file mode 100644 index 0000000..61893de --- /dev/null +++ b/plugins/ib/market-price.rb @@ -0,0 +1,133 @@ +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 `place_order`. +# +# The result can be customized by a provided block. +# +# 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_-attribute of IB::Contract + # (volatile, ie. data are not preserved when the Object is copied) + # + #Example: IB::Stock.new(symbol: :ge).market_price + # returns the current market-price + # + #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join + # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message + # and returns this value, too + # + + 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 out 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. + # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id } + + # 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..7399dca --- /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 (Blank for all available Exchanges) + ### 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-prototypes.rb b/plugins/ib/order-prototypes.rb new file mode 100644 index 0000000..188d0d9 --- /dev/null +++ b/plugins/ib/order-prototypes.rb @@ -0,0 +1,104 @@ +# These modules are used to facilitate referencing of most common Ordertypes + +module IB + 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 ].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..faaaf0b --- /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/all-in-one.rb b/plugins/ib/order_prototypes/all-in-one.rb new file mode 100644 index 0000000..1671beb --- /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..1671beb --- /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..0d1056f --- /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..4f226f6 --- /dev/null +++ b/plugins/ib/order_prototypes/limit.rb @@ -0,0 +1,177 @@ + +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. + 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..9246a5e --- /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..8f9c31f --- /dev/null +++ b/plugins/ib/order_prototypes/pegged.rb @@ -0,0 +1,173 @@ +module IB + module Pegged2Primary + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'REL' , 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' + end + + def aliases + Limit.aliases.merge limit_price: :stock_reference_price + 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 limit_price: 'Stock Reference Price', + stock_ref_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. + ------------ + Supported Exchanges: (as of Jan 2018): BOX, NASDAQOM, PHLX + HERE + end + end + end + + module Pegged2Benchmark + extend OrderPrototype + class << self + + def defaults + super.merge order_type: 'PEG BENCH' + end + + + + + + def requirements + super.merge total_quantity: :decimal, + delta: 'required Delta of the Option', + starting_price: 'initial Limit-Price for the Option' , + is_pegged_change_amount_decrease: 'increase(true) / decrease(false) Price', + pegged_change_amount: ' (increase/decrceas) 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', + reference_exchange: "Exchange of the reference contract" + + + + + + 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' + 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..f4afc11 --- /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..2bf2bb0 --- /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..0e7d674 --- /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..e90c8d9 --- /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 == '' + 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/spread-prototypes.rb b/plugins/ib/spread-prototypes.rb new file mode 100644 index 0000000..c0156c4 --- /dev/null +++ b/plugins/ib/spread-prototypes.rb @@ -0,0 +1,64 @@ +# These modules are used to facilitate referencing of most common Spreads + +# Spreads are created in two ways: +# +# (1) IB::Spread::{prototype}.build from: {underlying}, +# trading_class: (optional) +# {other specific attributes} +# +# (2) IB::Spread::{prototype}.fabcricate master: [one leg}, +# {other specific attributes} +# +# They return a freshly instantiated Spread-Object +# +module IB + module SpreadPrototype + + + def build from: , **fields + end + + + def initialize_spread ref_contract = nil, **attributes + error "Initializing of Spread failed – contract is missing" unless ref_contract.is_a?(IB::Contract) + # make sure that :exchange, :symbol and :currency are present + the_contract = ref_contract.merge( **attributes ).verify.first + error "Underlying for Spread is not valid: #{ref_contract.to_human}" if the_contract.nil? + the_spread= IB::Spread.new the_contract.attributes.slice( :exchange, :symbol, :currency ) + error "Initializing of Spread failed – Underling is no Contract" if the_spread.nil? + yield the_spread if block_given? # yield outside mutex controlled verify-environment + the_spread # return_value + end + + def requirements + {} + end + + def defaults + {} + end + + def optional + { } + end + + def parameters + the_output = ->(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 + +#require 'ib/spread_prototypes/straddle' +#require 'ib/spread_prototypes/strangle' +#require 'ib/spread_prototypes/vertical' +#require 'ib/spread_prototypes/calendar' +#require 'ib/spread_prototypes/stock-spread' +#require 'ib/spread_prototypes/butterfly' diff --git a/plugins/ib/spread_prototypes/butterfly.rb b/plugins/ib/spread_prototypes/butterfly.rb new file mode 100644 index 0000000..3a3d164 --- /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..e05f659 --- /dev/null +++ b/plugins/ib/spread_prototypes/calendar.rb @@ -0,0 +1,87 @@ +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 by just changing 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 + 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 + calendar = m.roll expiry: back + error "Initialisation of Legs failed" if calendar.legs.size != 2 + calendar.description = the_description( calendar ) + calendar # 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:, **fields + underlying = if from.is_a? IB::Option + fields[:right] = from.right unless fields.key?(:right) + fields[:front] = from.expiry unless fields.key(:front) + fields[:strike] = from.strike unless fields.key?(:strike) + 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).verify.first.essential + else + from + end + kind = { :front => fields.delete(:front), :back => fields.delete(:back) } + error "Specifiaction of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) + initialize_spread( underlying ) do | the_spread | + leg_prototype = IB::Option.new underlying.attributes + .slice( :currency, :symbol, :exchange) + .merge(defaults) + .merge( fields ) + kind[:back] = IB::Spread.transform_distance kind[:front], kind[: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 ) + 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( &:last_trading_day )].transpose + "" + 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..a7c47f3 --- /dev/null +++ b/plugins/ib/spread_prototypes/stock-spread.rb @@ -0,0 +1,47 @@ +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{|y| y.is_a?( IB::Stock ) ? y.merge(**args) : IB::Stock.new( symbol: y ).merge(**args)} + 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 + + 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 + { combo_params: ['NonGuaranteed', true] } + 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..f2a0a87 --- /dev/null +++ b/plugins/ib/spread_prototypes/straddle.rb @@ -0,0 +1,68 @@ +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 + the_spread.add_leg( master.essential.merge( right: flip_right[master.right], local_symbol: "") ) + 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) + .merge(defaults) + .merge( fields ) + + 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::Future.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 + "" + 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..844afad --- /dev/null +++ b/plugins/ib/spread_prototypes/strangle.rb @@ -0,0 +1,95 @@ +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 + 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 ) ) + 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:, **fields + underlying = if from.is_a? IB::Option + fields[:p] = from.strike unless fields.key?(:p) || from.right == :call + fields[:c] = from.strike unless fields.key?(:c) || from.right == :puta + 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 = { :p => fields.delete(:p), :c => fields.delete(:c) } + initialize_spread( underlying ) do | the_spread | + leg_prototype = IB::Option.new from.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( 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..0ac40d2 --- /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/verify.rb b/plugins/ib/verify.rb new file mode 100644 index 0000000..470a947 --- /dev/null +++ b/plugins/ib/verify.rb @@ -0,0 +1,190 @@ +module IB + 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 specified, 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 + _verify &b + 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 + + # + # depreciated: Do not use anymore + def verify! + c = 0 + IB::Connection.logger.warn "Contract.verify! is depreciated. Use \"contract = contract.verify.first\" instead" + c= verify.first + self.attributes = c.invariant_attributes + self.contract_detail = c.contract_detail + self + 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.close + 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 + + while r = queue.pop + received_contracts << r + 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/ib/connection_spec.rb b/spec/ib/connection_spec.rb index dd6542c..cf8e4ae 100644 --- a/spec/ib/connection_spec.rb +++ b/spec/ib/connection_spec.rb @@ -14,3 +14,20 @@ 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], connect: false + expect( c ).to be_a IB::Connection + c.connect + 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', connect: false + expect( c ).to be_a IB::Connection + expect{ c.connect }.to raise_error Errno::ECONNREFUSED + + end +end + diff --git a/spec/ib/plugins_spec.rb b/spec/ib/plugins_spec.rb new file mode 100644 index 0000000..e0c44a5 --- /dev/null +++ b/spec/ib/plugins_spec.rb @@ -0,0 +1,44 @@ +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 == [] } + Then { expect{ current.activate_plugin('invalid') }.to raise_error IB::Error } + + end + + context "Verify Plugin" do + let( :stock ) { IB::Stock.new symbol: 'M' } + + it "Prior to the activation of the verify plugin" do + expect{ stock.verify }.to raise_error NoMethodError + end + + it " Activated Verify Plugin " 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/main_helper.rb b/spec/main_helper.rb index 90f5098..8e8d3b3 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -68,14 +68,14 @@ def clean_connection puts " Logs:", log_entries if @stdout end @stdout.string = '' if @stdout - ib.clear_received + ib.clear_received end end def close_connection ib = IB::Connection.current if ib - clean_connection + clean_connection ib.close end end diff --git a/spec/spec.yml b/spec/spec.yml index 7917c5a..25f25a1 100644 --- a/spec/spec.yml +++ b/spec/spec.yml @@ -1,7 +1,9 @@ --- :connection: - :port: 4002 # 7497 or 4001 / 7496 - :host: 127.0.0.1 + :port: 4002 # 7497 or 4001 / 7496 + :host: 127.0.0.1 + # :port: 7496 # 4002 # 7497 or 4001 / 7496 + #: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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 21a09b3..5ef2f3f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,7 +25,7 @@ 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:" From 01e3ef06faa0b13a9d3ee9490b04b61e3f7a7ec2 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 11 Apr 2024 09:57:54 +0200 Subject: [PATCH 21/76] Update README.md --- README.md | 44 +------------------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/README.md b/README.md index 058ff7e..ea05057 100644 --- a/README.md +++ b/README.md @@ -79,53 +79,11 @@ Currently implemented plugins * probability-of-expiring: calculate the probability of expiring for the option-contract -##### User-specific Actions -Besides storing any TWS-response in an array, callbacks are implemented. -The user subscribes to a certain response and defines the actions in a typically ruby manner. These actions -can be defined globally -```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 - -``` - -or occasionally - -```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 From fd2ccb8f2299004f46d9027e1d4c679b11bde365 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 11 Apr 2024 09:59:10 +0200 Subject: [PATCH 22/76] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ea05057..5ab9fa2 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,6 @@ Currently implemented plugins * probability-of-expiring: calculate the probability of expiring for the option-contract - -``` ## Minimal TWS-Version `ib-api` is tested via the _stable IB-Gateway_ (Version 10.19) and should work with any current tws-installation. From dc13dd1c7e4afe5371026c5cdf70edada87a2300 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 12 Apr 2024 09:39:28 +0200 Subject: [PATCH 23/76] including connection tools and managed-accounts plugins --- README.md | 2 + bin/console | 14 +- bin/console.yml | 4 +- lib/ib/connection.rb | 13 +- plugins/ib/connection-tools.rb | 86 ++++++++++++ plugins/ib/greeks.rb | 4 +- plugins/ib/managed-accounts.rb | 239 +++++++++++++++++++++++++++++++++ 7 files changed, 346 insertions(+), 16 deletions(-) create mode 100644 plugins/ib/connection-tools.rb create mode 100644 plugins/ib/managed-accounts.rb diff --git a/README.md b/README.md index 5ab9fa2..62831c6 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ puts g.verify.first.attributes Currently implemented plugins +* connection-tools: ensure that a connection is established and active * verify: get contract details from the tws +* managed-accounts: fetch and organize account- and portfoliovalues * market-price: fetch the current market-price of a contract * eod: retrieve EOD-Data for the given contract * greeks: read current option greeks diff --git a/bin/console b/bin/console index 53a934c..c99ba82 100755 --- a/bin/console +++ b/bin/console @@ -48,8 +48,8 @@ read_yml = -> (key) do include IB require 'irb' client_id = ARGV[1] || read_yml[:client_id] - specified_port = ARGV[0] || 'Gateway' - port = case specified_port + specified_host = ARGV[0] || 'Gateway' + host = case specified_host when Integer specified_port # just use the number when /^[gG]/ @@ -63,8 +63,10 @@ read_yml = -> (key) do ## 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 = Connection.new client_id: client_id, host: host, connect: false do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| + c.activate_plugin 'connection-tools' + c.activate_plugin 'verify' + c.activate_plugin 'managed-accounts' 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 }} @@ -75,13 +77,15 @@ read_yml = -> (key) do # end c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } + + c.initialize_managed_accounts 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 on Port #{port}, client_id #{client_id} used" + puts "Connection established on #{host}, client_id #{client_id} used" puts puts "----> C points to the connection-instance" puts diff --git a/bin/console.yml b/bin/console.yml index 392bd8b..8e0eda6 100644 --- a/bin/console.yml +++ b/bin/console.yml @@ -1,3 +1,3 @@ -:gateway: 4002 -:tws: 7496 +:gateway: "localhost:4002" +:tws: "10.247.8.109:7496" :client_id: 2000 diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 6238e30..21ee365 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -57,8 +57,6 @@ def initialize host: '127.0.0.1', v = eval(k.to_s) instance_variable_set("@#{k}", v) unless v.nil? end - puts "@host: #{@host}" - puts "@port: #{@port}" # A couple of locks to avoid race conditions in JRuby @subscribe_lock = Mutex.new @@ -69,8 +67,6 @@ def initialize host: '127.0.0.1', activate_plugin name end - - @connected = false self.next_local_id = nil @@ -86,9 +82,8 @@ def initialize host: '127.0.0.1', yield self if block_given? if connect - disconnect if connected? update_next_order_id - Kernel.exit if self.next_local_id.nil? # emergency exit. + Kernel.exit if self.next_local_id.nil? # emergency exit. # update_next_order_id should have raised an error end Connection.current = self @@ -100,7 +95,11 @@ def update_next_order_id q = Queue.new subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id } unless connected? - connect() # connect implies requesting NextValidId + if @plugins.inlcude? `connection-tools` + safe_connect + else + connect() # connect implies requesting NextValidId + end else send_message :RequestIds end diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb new file mode 100644 index 0000000..ae3b7b7 --- /dev/null +++ b/plugins/ib/connection-tools.rb @@ -0,0 +1,86 @@ +module IB + 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. + # + # It delays the process by 6 ms (150 MBit Cable connection) + # + # a = Time.now; G.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 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 + disconnect + logger.info{"not connected ... trying to reconnect "} + sleep 0.1 + connect + count = 0 + retry + end + end + unsubscribe z + result # return value + end + + # Alternative to `Connection#connect'. + # + # Trys 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. + # + # + def safe_connect maximal_count_of_retry=100 + + i= -1 + begin + connect + 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 + true # return success-flag + end # def + end + + class Connection + include ConnectionTools + end +end diff --git a/plugins/ib/greeks.rb b/plugins/ib/greeks.rb index d67937f..98a30bc 100644 --- a/plugins/ib/greeks.rb +++ b/plugins/ib/greeks.rb @@ -12,10 +12,10 @@ module Greeks # def request_greeks delayed: true, what: :model, thread: false - tws = Connection.current # get the initialized ib-ruby instance + 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 ], + # 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 = [] diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb new file mode 100644 index 0000000..d234930 --- /dev/null +++ b/plugins/ib/managed-accounts.rb @@ -0,0 +1,239 @@ +module IB + +=begin + +Plugin for Managed Accounts + +Provides `clients` and `advisor` methods that contain account-specific data + +* 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. + + +Standard usage + + ib = Connection.new connect: false do | c | + c.activate_plugin 'managed-accounts' + c.initialize_managed_accounts + c.get_account_data + end + +=end + +module ManagedAccounts + +=begin +--------------------------- InitializeManageAccounts ---------------------------------- + +If initiated with the parameter `force: true`, a reconnect is performed to initiate the +transmission of available managed-accounts. + +=end + def initialize_managed_accounts( force: false ) + queue = Queue.new + # 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 } + @accounts = [] + + if connected? + disconnect + sleep(0.1) + end + if @plugins.include? 'connection-tools' + safe_connect + else + connect() + end + result = queue.pop + unsubscribe man_id, rec_id, error_id + @accounts + + end # def + +=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 + + 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 + rescue IB::TransmissionError => e + tws.unsubscribe download_end unless download_end.nil? + tws.unsubscribe subscription + raise + end + + + def all_contracts + clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id) + end + + + private + + # 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 + # + + def subscribe_account_updates continuously: true + 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 + # simply don't cancel the subscription if continuously is specified + # the connected flag is set in any case, indicating that valid data are present + # tws.send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously + 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 } + account.portfolio_values << msg.portfolio_value +# msg.portfolio_value.account = account +# # link contract -> portfolio value +# account.contracts.find{ |x| x.con_id == msg.contract.con_id } +# .portfolio_values +# .update_or_create( msg.portfolio_value ) { :account } + IB::Connection.logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" } + end # case + end # account_data + end # subscribe + end # def + + + 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 + + end + + class Connection + include ManagedAccounts + end +end From d3b138b05cd9b0b9e6d943b808636d5cf547f9ae Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 12 Apr 2024 13:48:32 +0200 Subject: [PATCH 24/76] Typo while including connection-tools --- lib/ib/connection.rb | 2 +- plugins/ib/managed-accounts.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 21ee365..02d6869 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -95,7 +95,7 @@ def update_next_order_id q = Queue.new subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id } unless connected? - if @plugins.inlcude? `connection-tools` + if @plugins.include? "connection-tools" safe_connect else connect() # connect implies requesting NextValidId diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index d234930..f96e363 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -67,7 +67,7 @@ def initialize_managed_accounts( force: false ) disconnect sleep(0.1) end - if @plugins.include? 'connection-tools' + if @plugins.include? "connection-tools" safe_connect else connect() From 6527aa142bc26843e7433f2ba715cf147be780ba Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 12 Apr 2024 17:14:26 +0200 Subject: [PATCH 25/76] Documentation for plugins --- README.md | 7 +++---- plugins/ib/connection-tools.rb | 9 +++++++++ plugins/ib/eod.rb | 10 ++++++++++ plugins/ib/greeks.rb | 9 +++++++++ plugins/ib/order-prototypes.rb | 10 ++++++++-- plugins/ib/spread-prototypes.rb | 22 ++++++++++++++-------- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 62831c6..d963e8d 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,15 @@ Ruby interface to Interactive Brokers' TWS API Reimplementation of the basic functions of ib-ruby --- -__STATUS: Preparing for a new GEM-Release) +__STATUS: Placement of orders is currently broken__ + --- __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. ---- diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index ae3b7b7..bbc8435 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -1,4 +1,13 @@ module IB + +=begin +Plugin for advanced Connections + +Provides `check_connection` and `safe_connect` + + +=end + module ConnectionTools # Handy method to ensure that a connection is established and active. # diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb index 4e880f0..0f118cd 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -2,6 +2,16 @@ module IB require 'active_support/core_ext/date/calculations' require 'csv' + +=begin + +Plugin to support EndOfDay OHLC-Data for a contract + +Provides Contract.eod for EndOfDay historical data, + Contract.get_bars for custom ohlc-timeframes, + Contract.from_csv and Contract.to_csv to store and retrieve ohlc-data +=end + module Eod module BuisinesDays # https://stackoverflow.com/questions/4027768/calculate-number-of-business-days-between-two-days diff --git a/plugins/ib/greeks.rb b/plugins/ib/greeks.rb index 98a30bc..3e82ec0 100644 --- a/plugins/ib/greeks.rb +++ b/plugins/ib/greeks.rb @@ -1,4 +1,13 @@ 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 diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order-prototypes.rb index 188d0d9..c1d06a8 100644 --- a/plugins/ib/order-prototypes.rb +++ b/plugins/ib/order-prototypes.rb @@ -1,6 +1,12 @@ -# These modules are used to facilitate referencing of most common Ordertypes - module IB +=begin + # Plugin to build IB::Order objects through singletons + # Limit.order size: 4, price: 10 + # Market.order: size: 4 + # etc + # + +=end module OrderPrototype diff --git a/plugins/ib/spread-prototypes.rb b/plugins/ib/spread-prototypes.rb index c0156c4..281148e 100644 --- a/plugins/ib/spread-prototypes.rb +++ b/plugins/ib/spread-prototypes.rb @@ -1,5 +1,18 @@ -# These modules are used to facilitate referencing of most common Spreads +=begin +Plugin to automate the creation of common spreads + + Straddle.build from: Contract, expiry:, strike: + Strangle build from: Contract. expiry:, p: , c: + Vertical.build from: Contract, expiry:, right: , buy: (a strike), sell: (a strike) + Calendar.build from: Contract. right:, :strike:, front: (an expiry), back: (an expiry) + Butterfly.buiild from: Contract, right:, strike: , expiry: , front: (long-option strike), back: (long option strike) + + StockSpread.fabricate symbol1, symbol2, ratio:[ n, m ] # only for us-stocks + +=end + +module IB # Spreads are created in two ways: # # (1) IB::Spread::{prototype}.build from: {underlying}, @@ -11,7 +24,6 @@ # # They return a freshly instantiated Spread-Object # -module IB module SpreadPrototype @@ -56,9 +68,3 @@ def parameters end end -#require 'ib/spread_prototypes/straddle' -#require 'ib/spread_prototypes/strangle' -#require 'ib/spread_prototypes/vertical' -#require 'ib/spread_prototypes/calendar' -#require 'ib/spread_prototypes/stock-spread' -#require 'ib/spread_prototypes/butterfly' From 363cd9b8955b61fa2210bafc25592fe5c7058f65 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 23 Apr 2024 09:03:57 +0200 Subject: [PATCH 26/76] Including "roll" plugin for Options and Futures. Redesign of Option.next_expiry and Future.next_expiry --- lib/ib/connection.rb | 2 +- lib/ib/messages/incoming.rb | 2 +- lib/ib/socket.rb | 8 +-- lib/ib/support.rb | 18 +++--- models/ib/future.rb | 57 +++++++++++++------ models/ib/option.rb | 53 ++++++++++++++++- models/ib/spread.rb | 51 ++++++++--------- plugins/ib/roll.rb | 57 +++++++++++++++++++ plugins/ib/spread_prototypes/calendar.rb | 3 +- plugins/ib/spread_prototypes/straddle.rb | 8 ++- .../incoming/account_update_multi_spec.rb | 2 +- 11 files changed, 197 insertions(+), 64 deletions(-) create mode 100644 plugins/ib/roll.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 02d6869..bcba9a7 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -419,7 +419,7 @@ def subscribers # Process single incoming message (blocking!) def process_message - logger.progname='IB::Connection#process_message' if logger.is_a?(Logger) + logger.progname='IB::Connection#process_message' socket.decode_message( socket.receive_messages ) do | the_decoded_message | # puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}" diff --git a/lib/ib/messages/incoming.rb b/lib/ib/messages/incoming.rb index 9ea541a..4a08c88 100644 --- a/lib/ib/messages/incoming.rb +++ b/lib/ib/messages/incoming.rb @@ -170,7 +170,7 @@ def to_human [ :account , :string ], [ :model, :string ], [ :key , :string ], - [ :value , :decimal], + [ :value , :float], [ :currency, :string ]) AccountSummary = def_message(63, AccountMessage, [:request_id, :int], diff --git a/lib/ib/socket.rb b/lib/ib/socket.rb index e3c8753..1109e96 100644 --- a/lib/ib/socket.rb +++ b/lib/ib/socket.rb @@ -63,18 +63,18 @@ def receive_messages complete_message_buffer = [] begin # this is the blocking version of recv - buffer = self.recvfrom(4096)[0] -# STDOUT.puts "BUFFER:: #{buffer.inspect}" + buffer = self.recvfrom(8192)[0] + # STDOUT.puts "BUFFER:: #{buffer.inspect}" complete_message_buffer << buffer - end while buffer.size == 4096 + 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 + Kernel.exit end end diff --git a/lib/ib/support.rb b/lib/ib/support.rb index b44a9ec..d912526 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -111,32 +111,32 @@ def read_date # Without providing a Block, the elements are treated as string def read_array hashmode:false, &block count = read_int - case count - when 0 + case count + when 0 [] when nil nil else count= count + count if hashmode if block_given? - Array.new(count, &block) + Array.new(count, &block) else Array.new( count ){ read_string } end - end + end end - # - # Returns a hash - # Expected Buffer-Format: + # + # 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? + result = if tags.nil? || tags.flatten.empty? tags else - interim = if tags.size.modulo(2).zero? + interim = if tags.size.modulo(2).zero? Hash[*tags.flatten] else Hash[*tags[0..-2].flatten] # omit the last element diff --git a/models/ib/future.rb b/models/ib/future.rb index 87698a2..5ac129d 100644 --- a/models/ib/future.rb +++ b/models/ib/future.rb @@ -9,29 +9,54 @@ 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::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 time=Time.now - [3, 6, 9, 12].find { |month| month > time.month } || 3 # for December, next March + def next_quarter_month d + [3, 6, 9, 12].find { |month| month > d.month } || 3 # for December, next March end - def next_quarter_year time=Time.now - next_quarter_month(time) < time.month ? time.year + 1 : time.year + def next_quarter_year d + next_quarter_month(d) < d.month ? d.year + 1 : d.year end - - # WARNING: This returns the next - # quarterly expiration month after the current month. Many futures - # instruments have monthly contracts for the near months. This - # method will not work for such contracts; it will return the next - # quarter after the current month, even though the present month - # has the majority of the trading volume. - # - def next_expiry time=Time.now - "#{ next_quarter_year(time) }#{ sprintf("%02d", next_quarter_month(time)) }" - end - end end end diff --git a/models/ib/option.rb b/models/ib/option.rb index a28c920..c3e43d9 100644 --- a/models/ib/option.rb +++ b/models/ib/option.rb @@ -67,13 +67,64 @@ def == other 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 + + # 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 + class FutureOption < Option def default_attributes super.merge :sec_type => :futures_option end diff --git a/models/ib/spread.rb b/models/ib/spread.rb index f667967..54e3a6a 100644 --- a/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -9,7 +9,7 @@ class Spread < Bag 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 @@ -17,8 +17,8 @@ class Spread < Bag 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 + 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) @@ -35,9 +35,9 @@ def self.transform_distance front, back end else nb - end - end # def - + end + end # def + def to_human self.description end @@ -45,18 +45,18 @@ def to_human 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 + 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], + 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 @@ -64,13 +64,13 @@ def fake_portfolio_position( array_of_portfolio_values ) # adds a leg to any spread # - # Parameter: + # Parameter: # contract: Will be verified. Contract.essential is added to legs-array # action: :buy or :sell # weight: # ratio: - # - # Default: action: :buy, weight: 1 + # + # 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 @@ -106,7 +106,7 @@ def remove_leg contract_or_position = nil # 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} @@ -118,7 +118,7 @@ def essential 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 @@ -135,7 +135,7 @@ def non_guaranteed= x def non_guaranteed - combo_params['NonGuaranteed'] + combo_params['NonGuaranteed'] end # optional: specify default order prarmeters for all spreads # def order_requirements @@ -143,9 +143,9 @@ def non_guaranteed # end - def as_table + def as_table t= Terminal::Table.new title: description[1..-2] , - headings: table_header, + headings: table_header, style: { border: :unicode } @@ -156,7 +156,7 @@ def as_table end def self.build_from_json container - read_leg = ->(a) do + read_leg = ->(a) do IB::ComboLeg.new :con_id => a.read_int, :ratio => a.read_int, :action => a.read_string, @@ -165,13 +165,10 @@ def self.build_from_json container 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.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/plugins/ib/roll.rb b/plugins/ib/roll.rb new file mode 100644 index 0000000..f0c46b0 --- /dev/null +++ b/plugins/ib/roll.rb @@ -0,0 +1,57 @@ +module IB + module RollFuture + # helper method to roll an existing future + # + # Argument is the expiry of the target-future. + # + + def roll **args + error "specify expiry to roll a future" if args.empty? + args[:to] = args[:expiry] if args[:expiry].present? && args[:expiry] =~ /[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 + target.add_leg self, action: :buy + target.add_leg new_future, action: :sell + end + end + + + module RollOption + # helper method to roll an existing option + # + # Arguments are strike and expiry of the target-option. + # + # Example: ge= Symbols::Options.ge.verify.first.roll( strike: 130 ) + # ge.to_human + # => " added added " + # + # rolls the Option to another strike + + 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 ).verify.first + 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 + end + end + + Connection.current.activate_plugin 'verify' + + class Future + include RollFuture + end + + class Option + include RollOption + end +end diff --git a/plugins/ib/spread_prototypes/calendar.rb b/plugins/ib/spread_prototypes/calendar.rb index e05f659..0bf78ce 100644 --- a/plugins/ib/spread_prototypes/calendar.rb +++ b/plugins/ib/spread_prototypes/calendar.rb @@ -8,7 +8,7 @@ class << self # Fabricate a Calendar-Spread from a Master-Option # ----------------------------------------- -# If one Leg is known, the other is build by just changing the expiry +# If one Leg is known, the other is build through replacing the expiry # The second leg is always SOLD ! # # Call with @@ -17,6 +17,7 @@ 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 calendar = m.roll expiry: back diff --git a/plugins/ib/spread_prototypes/straddle.rb b/plugins/ib/spread_prototypes/straddle.rb index f2a0a87..2bc319c 100644 --- a/plugins/ib/spread_prototypes/straddle.rb +++ b/plugins/ib/spread_prototypes/straddle.rb @@ -33,13 +33,14 @@ def fabricate master # 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) + fabricate from.merge **fields else initialize_spread( from ) do | the_spread | - leg_prototype = IB::Option.new from.attributes + leg_prototype = IB::Option.new from.invariant_attributes .slice( :currency, :symbol, :exchange) .merge(defaults) .merge( fields ) + 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 @@ -60,7 +61,8 @@ def requirements end def the_description spread - "" + my_strike = spread.legs.first.strike + "" end end # class diff --git a/spec/ib/messages/incoming/account_update_multi_spec.rb b/spec/ib/messages/incoming/account_update_multi_spec.rb index edf3d34..835237a 100644 --- a/spec/ib/messages/incoming/account_update_multi_spec.rb +++ b/spec/ib/messages/incoming/account_update_multi_spec.rb @@ -3,7 +3,7 @@ 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( :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 } From bb9bee9966b7224fa89bbfd6a0dfa9ece62c664b Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 23 Apr 2024 20:53:28 +0200 Subject: [PATCH 27/76] IB::Straddle:: force verification of contracts --- plugins/ib/spread_prototypes/straddle.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/ib/spread_prototypes/straddle.rb b/plugins/ib/spread_prototypes/straddle.rb index 2bc319c..5bc04aa 100644 --- a/plugins/ib/spread_prototypes/straddle.rb +++ b/plugins/ib/spread_prototypes/straddle.rb @@ -16,8 +16,8 @@ def fabricate master 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 - the_spread.add_leg( master.essential.merge( right: flip_right[master.right], local_symbol: "") ) + 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 From 3ee1723621481d19b7bfe52c77f2ce6a4bfc69b2 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 26 Apr 2024 21:37:57 +0200 Subject: [PATCH 28/76] Including plugins 'advance-account', 'auto-adjust', 'managed-accounts' and 'roll' --- models/ib/option.rb | 31 ++- models/ib/order.rb | 40 ++-- models/ib/spread.rb | 2 +- plugins/ib/advanced-account.rb | 372 +++++++++++++++++++++++++++++++++ plugins/ib/auto-adjust.rb | 53 +++++ plugins/ib/managed-accounts.rb | 6 +- plugins/ib/roll.rb | 23 +- 7 files changed, 489 insertions(+), 38 deletions(-) create mode 100644 plugins/ib/advanced-account.rb create mode 100644 plugins/ib/auto-adjust.rb diff --git a/models/ib/option.rb b/models/ib/option.rb index c3e43d9..f39327a 100644 --- a/models/ib/option.rb +++ b/models/ib/option.rb @@ -68,19 +68,34 @@ def == other end - # get the next (regular) expiry of the contract + # returns the verified option for the next (regular) expiry of the contract. # - # fetches for real contracts if verify is available + # 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) # def next_expiry d = Date.today - exp = self.class.next_expiry d + # 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' - self.expiry = exp[0..-3] - verify.sort_by{| x | x.last_trading_day} - .find_all{| y | y.expiry <= exp } - .first + 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" ) if exp[-2..-1] == "00" + end + real_option else - exp + merge( expiry: exp, last_trading_day: Date.parse( exp ).strftime( "%Y-%m-%d" ) ) end end diff --git a/models/ib/order.rb b/models/ib/order.rb index f372847..e8e917b 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -84,7 +84,7 @@ class Order < IB::Base # 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. @@ -153,12 +153,12 @@ class Order < IB::Base :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. + # 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). + # 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 + # 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, @@ -207,7 +207,7 @@ class Order < IB::Base :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. @@ -217,20 +217,20 @@ class Order < IB::Base :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. + # 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. + # 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 + # 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, @@ -271,10 +271,10 @@ class Order < IB::Base :modified_at, :leg_prices, :algo_params, - :combo_params # Valid tags are LeginPrio, MaxSegSize, DontLeginNext, ChangeToMktTime1, - # ChangeToMktTime2, ChangeToMktOffset, DiscretionaryPct, NonGuaranteed, + :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: + # 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 @@ -301,7 +301,7 @@ class Order < IB::Base # Order has a collection of OrderStates, 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 @@ -354,7 +354,7 @@ def order_state= state validates_numericality_of :limit_price, :aux_price, :allow_nil => true - def default_attributes # default valus are taken from order.java + def default_attributes # default valus are taken from order.java # public Order() { } super.merge( :active_start_time => "", # order.java # 470 # Vers 69 @@ -374,7 +374,7 @@ def default_attributes # default valus are taken from order.java :not_held => false, # order.java # 494 :oca_type => :none, :order_type => :limit, - :open_close => :open, # order.java # + :open_close => :open, # order.java # :opt_out_smart_routing => false, :origin => :customer, :outside_rth => false, # order.java # 472 @@ -393,7 +393,7 @@ def default_attributes # default valus are taken from order.java :leg_prices => [], :algo_params => Hash.new, #{}, :combo_params =>[], #{}, - # :soft_dollar_tier_params => HashWithIndifferentAccess.new( + # :soft_dollar_tier_params => HashWithIndifferentAccess.new( # :name => "", # :val => "", # :display_name => ''), @@ -441,7 +441,7 @@ def serialize_algo # end def serialize_misc_options - "" # Vers. 70 + "" # Vers. 70 end # Placement # diff --git a/models/ib/spread.rb b/models/ib/spread.rb index 54e3a6a..3b30f55 100644 --- a/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -21,7 +21,7 @@ def self.transform_distance front, back nb = if back.to_i > 200000 back.to_i elsif back[-1] == "w" && front.to_i > 20000000 - start_date + (back.to_i * 7) + 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 diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb new file mode 100644 index 0000000..0961333 --- /dev/null +++ b/plugins/ib/advanced-account.rb @@ -0,0 +1,372 @@ +module IB + + module Advanced + + + def account_data_scan search_key, search_currency=nil + if account_values.is_a? Array + 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 + + else # not tested!! + if search_currency.present? + account_values.where( ['key like %', search_key] ).where( currency: search_currency ) + else # any currency + account_values.where( ['key like %', search_key] ) + end + 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 Orderrecord 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 + else + orders.find_all{|x| x[search_option.first].to_i == search_option.last.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::Gateway.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=>"" + + the_local_id = g.place order: order + => 67 # returns local_id + order.contract # updated 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: the_local_id ) + => 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 + # adjust the orderprice to min-tick + result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } + #·IB::Symbols are always qualified. They carry a description-field + qualified_contract = ->(c) { c.is_a?(IB::Contract) && ( c.description.present? || !c.con_id.to_i.zero? || (c.con_id.to_i <0 && c.sec_type == :bag )) } + + order.contract ||= if qualified_contract[ contract ] + contract + else + contract.verify.first + end + + 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] + + ib = IB::Connection.current + wrong_order = nil + the_local_id = nil + q = Queue.new + + ### Handle Error messages + ### Default action: raise IB::Transmission Error + sa = ib.subscribe( :Alert ) do | msg | + # puts "local_id: #{the_local_id}"a + puts msg.inspect + if msg.error_id == the_local_id + if [ 110, # The price does not confirm to the minimum price variation for this contract + 201, # Order rejected, No Trading permissions + 203, # Security is not allowed for trading + 325, # Disretionary Orders are not supported for ths combination of oerder-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 + wrong_order = msg.message + ib.logger.error msg.message + q.close # closing the queue indicates that no order was transmitted + end + end + end + 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.update_or_create order, :order_ref + order.auto_adjust if respond_to?( :auto_adjust ) && auto_adjust # /defined in file order_handling.rb + if convert_size + order.action = order.total_quantity.to_i < 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_i < 0 + order.total_quantity = order.total_quantity.to_i.abs + end + # apply non_guarenteed and other stuff bound to the contract to order. + order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank? + # 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 + the_contract = order.contract.con_id.to_i >0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil + the_local_id = order.place the_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 + + ib.unsubscribe sa + ib.unsubscribe sb + if q.closed? + if wrong_order.present? + raise IB::SymbolError, wrong_order + elsif the_local_id.present? + order.local_id = the_local_id + else + error " #{order.to_human} is not transmitted properly", :symbol + end + else + order=tws_answer # return order-record received from tws + end + the_local_id # return_value + 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 modificable. + 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 manualy in the provided block +=end + + + def modify_order local_id: nil, order_ref: nil, order: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 order_state.forcast + # + # The order received from the TWS is 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 + the_local_id = nil + req = ib.subscribe( :OpenOrder ){|m| q << m.order if m.order.local_id.to_i == the_local_id.to_i } + + result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } + order.what_if = true + order.account = account + the_local_id = order.place contract + Thread.new{ sleep 2 ; q.close } + 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? + returned_order.order_state.forcast # return_value + end + +# closes the contract by submitting an appropiate 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 opposide position is established. + # Any value in total_quantity is overwritten + # + # returns the order transmitted + # + # raises an IB::Error if no PortfolioValues have been loaded to the IB::Acoount + 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 + + contract &.verify{|c| order.contract = c} # if contract is specified: don't touch the parameter, get a new object . + 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: + Gateway.current.cancel_order order + end + + #returns an hash where portfolio_positions are grouped into Watchlists. + # + # Watchlist => [ contract => [ portfoliopositon] , ... ] ] + # + def organize_portfolio_positions the_watchlists= IB::Gateway.current.active_watchlists + the_watchlists = [ the_watchlists ] unless the_watchlists.is_a?(Array) + self.focuses = portfolio_values.map do | pw | + z= the_watchlists.map do | w | + ref_con_id = pw.contract.con_id + watchlist_contract = w.find do |c| + c.is_a?(IB::Bag) ? c.combo_legs.map(&:con_id).include?(ref_con_id) : c.con_id == ref_con_id + 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 +# # + +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..df5e682 --- /dev/null +++ b/plugins/ib/auto-adjust.rb @@ -0,0 +1,53 @@ + +module IB + module AutoAdjust + + + # Auto Adjust implements a simple algorithm to ensure that an order is accepted + + # It reads `contract_detail.min_tick`. + # # + # If min_tick < 0.01, the real tick-increments differ fron the min_tick_value + # + # For J36 (jardines) min tick is 0.001, but the minimal increment is 0.005 + # For Tui1 its the samme, min_tick is 0.00001 , minimal increment ist 0.00005 + # + # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit. + # + # | min-tick | round | + # |--------------|------------| + # | 10 | 110 | + # | 1 | 111 | + # | 0.1 | 111.1 | + # | 0.001 | 111.11 | + # | 0.0001 | 111.11 | + # | 0.00001 | 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 + # ensure that contract_details are present + + min_tick = contract.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_f, min_tick) unless limit_price.to_f.zero? + self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? + end + end + class Order + include AutoAdjust + end # class Order +end # module diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index f96e363..2dde29b 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -160,8 +160,8 @@ def get_account_data *accounts, **compatibily_argument send_message :RequestAccountData, subscribe: false ## do this only once unsubscribe subscription rescue IB::TransmissionError => e - tws.unsubscribe download_end unless download_end.nil? - tws.unsubscribe subscription + unsubscribe download_end unless download_end.nil? + unsubscribe subscription raise end @@ -199,7 +199,7 @@ def subscribe_account_updates continuously: true if account.account_values.size > 10 # simply don't cancel the subscription if continuously is specified # the connected flag is set in any case, indicating that valid data are present - # tws.send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously + # send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously 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 diff --git a/plugins/ib/roll.rb b/plugins/ib/roll.rb index f0c46b0..7a508f8 100644 --- a/plugins/ib/roll.rb +++ b/plugins/ib/roll.rb @@ -20,28 +20,39 @@ def roll **args module RollOption - # helper method to roll an existing option + # helper method to roll an existing short-poption # # Arguments are strike and expiry of the target-option. # - # Example: ge= Symbols::Options.ge.verify.first.roll( strike: 130 ) - # ge.to_human - # => " added added " + # Example: r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( strike: 1900 ) + # r.to_human + # => " rolling to " + # r.combo_legs.to_human + # => ["", ""] # - # rolls the Option to another strike + # 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 ).verify.first + 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 Date: Sat, 27 Apr 2024 09:53:44 +0200 Subject: [PATCH 29/76] **Testing of Order parameters ** --- lib/ib/messages/incoming/open_order.rb | 2 +- lib/ib/messages/outgoing/place_order.rb | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 7bf51a7..740a951 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -4,7 +4,7 @@ module Incoming 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) + def_message [5, 0], # updated to v. 34 according to python (decoder.py processOpenOrder) [:order, :local_id, :int], [:contract, :contract], # read standard-contract diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index d6bc4a4..4f74cdf 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -6,8 +6,8 @@ module Outgoing # 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 + PlaceOrder = def_message [ 3,0 ] ## ServerVersion > 145 && < 163: def_message[ 3,45 ] + ## server-version is not known at compilation time ## Method call has to be replaced then ## Max-Client_ver --> 144!! @@ -23,7 +23,7 @@ def encode # main order fields (order.side == :short ? 'SSHORT' : order.side == :short_exempt ? 'SSHORTX' : order.side.to_sup), - order.total_quantity, + order.total_quantity.to_d, order[:order_type], # Internal code, 'LMT' instead of :limit order.limit_price, order.aux_price, @@ -181,8 +181,9 @@ def encode order.is_O_ms_container, # end, # if server_version >= 148 # min_server_ver_d_peg_orders - order.discretionary_up_to_limit_price + order.discretionary_up_to_limit_price, # end ] + "" ] # # From 67897c0ba675b10efd65682ea6d1bae298cec8eb Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 29 Apr 2024 05:29:11 +0200 Subject: [PATCH 30/76] Update to server-versions, premilary changes to order and open_order (still testing) --- lib/class_extensions.rb | 2 +- lib/ib/messages/incoming/open_order.rb | 342 +++++++++++++------------ lib/server_versions.rb | 9 +- models/ib/order.rb | 17 +- plugins/ib/process-orders.rb | 182 +++++++++++++ 5 files changed, 378 insertions(+), 174 deletions(-) create mode 100644 plugins/ib/process-orders.rb diff --git a/lib/class_extensions.rb b/lib/class_extensions.rb index 6203765..9aecb8b 100644 --- a/lib/class_extensions.rb +++ b/lib/class_extensions.rb @@ -68,7 +68,7 @@ def to_bool case self.chomp.upcase when 'TRUE', 'T', '1' true - when 'FALSE', 'F', '0', '' + when 'FALSE', 'F', '0', '', Float::MAX false else error "Unable to convert #{self} to bool" diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 740a951..42ef024 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -5,65 +5,65 @@ module Incoming # OpenOrder is the longest message with complex processing logics OpenOrder = def_message [5, 0], # updated to v. 34 according to python (decoder.py processOpenOrder) - [:order, :local_id, :int], + [ :order, :local_id, :int], - [:contract, :contract], # read standard-contract + [ :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], + [ :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], #@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], + [ :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] class OpenOrder @@ -98,26 +98,26 @@ def conditions # 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) + @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,133 +125,141 @@ 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], + [ :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 |_| + [ :contract, :combo_legs, :array, proc do |_| IB::ComboLeg.new :con_id => @buffer.read_int, - :ratio => @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 ], + :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_init_level_size, :int ], + [ :order, :scale_subs_level_size, :int ], - [:order, :scale_price_increment, :decimal], - [proc { | | filled?(@data[:order][:scale_price_increment]) }, + [ :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, :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]) }, + [ :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] - ], + [ :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], + [ :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_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], + [ :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 ] - - + [ 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, :string ], + [ :order, :discretionary_up_to_limit_price, :string], + [ :order, :use_price_management_algo, :string], + [ :order, :duration, :int ] + [ :order, :post_to_ats, :int ] + [ :order, :auto_cancel_parent, :string] + # 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" @@ -259,7 +267,7 @@ def filled? value # 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/server_versions.rb b/lib/server_versions.rb index 74b0fd3..33f9c04 100644 --- a/lib/server_versions.rb +++ b/lib/server_versions.rb @@ -128,7 +128,14 @@ :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_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 diff --git a/models/ib/order.rb b/models/ib/order.rb index e8e917b..cbe2f72 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -211,13 +211,14 @@ class Order < IB::Base ### 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 @@ -237,8 +238,15 @@ class Order < IB::Base :mifid_2_decision_algo, :mifid_2_execution_maker, :mifid_2_execution_algo, - :dont_use_auto_price_for_hedge, - :discretionary_up_to_limit_price + :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 + + # Properties with complex processing logics prop :tif, # String: Time in Force (time to market): DAY/GAT/GTD/GTC/IOC @@ -264,8 +272,7 @@ class Order < IB::Base :opt_out_smart_routing => :bool, # Australian exchange only, default false :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, diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb new file mode 100644 index 0000000..2de86fe --- /dev/null +++ b/plugins/ib/process-orders.rb @@ -0,0 +1,182 @@ + +module IB + module ProcessOrders +=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 + def initialize_order_handling + tws.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.update_or_create msg.order_state, :status + end + + logger.info { "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.update_or_create msg.order, :local_id + this_account.contracts.first_or_create msg.contract, :con_id + # now save the order-record + msg.order.contract = msg.contract + this_account.orders.update_or_create 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.first_or_create( IB::OrderState.new( perm_id: o.perm_id, + local_id: o.local_id, + status: 'Filled' ), :status ) + # 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.error { "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 + + + def request_open_orders + + q = Queue.new + subscription = tws.subscribe( :OpenOrderEnd ) { q.push(true) } # signal succsess + 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 + + tws.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 + + + + +end # module + +class Connection + inlcude ProcessOrders +end + +end ## module IB + + +module IB + + class Order + # Auto Adjust implements a simple algorithm to ensure that an order is accepted + + # It reads `contract_detail.min_tick`. + # # + # If min_tick < 0.01, the real tick-increments differ fron the min_tick_value + # + # For J36 (jardines) min tick is 0.001, but the minimal increment is 0.005 + # For Tui1 its the samme, min_tick is 0.00001 , minimal increment ist 0.00005 + # + # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit. + # + # | min-tick | round | + # |--------------|------------| + # | 10 | 110 | + # | 1 | 111 | + # | 0.1 | 111.1 | + # | 0.001 | 111.11 | + # | 0.0001 | 111.11 | + # | 0.00001 | 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 + # ensure that contract_details are present + + min_tick = contract.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_f, min_tick) unless limit_price.to_f.zero? + self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? + end + end + end # class Order +end # module From eaf7fa5b72abd128839f5f2ca1f14e6cdc914448 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 3 Jun 2024 15:55:02 +0200 Subject: [PATCH 31/76] Open Order adjustments --- lib/ib/messages/incoming/open_order.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 42ef024..7e4c3e1 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -226,17 +226,20 @@ def load [ proc { !@data[ :order ][ :conditions ].blank? }, [ :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 ], # cpp -source: 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, :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 ], From e60abfdee932af582082bc362c80e7321757493e Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sun, 5 May 2024 20:20:14 +0200 Subject: [PATCH 32/76] writes socket messages to debug log --- lib/ib/messages/outgoing/abstract_message.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/ib/messages/outgoing/abstract_message.rb b/lib/ib/messages/outgoing/abstract_message.rb index c91ecd5..8fa8ff8 100644 --- a/lib/ib/messages/outgoing/abstract_message.rb +++ b/lib/ib/messages/outgoing/abstract_message.rb @@ -24,10 +24,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 From 2810268ab293b840a398dcbb2e96d4ca249a4942 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sun, 5 May 2024 20:25:36 +0200 Subject: [PATCH 33/76] serialize only underlying attributes within class --- models/ib/contract.rb | 2 +- models/ib/underlying.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/ib/contract.rb b/models/ib/contract.rb index 8cb67b5..cee1281 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -167,7 +167,7 @@ def serialize_supershort *fields # :nodoc: # Serialize under_comp parameters: EClientSocket.java, line 471 def serialize_under_comp *args # :nodoc: - under_comp ? under_comp.serialize : [false] + under_comp ? [true] + under_comp.serialize : [false] end # Defined in Contract, not BAG subclass to keep code DRY diff --git a/models/ib/underlying.rb b/models/ib/underlying.rb index feedd5a..11e242d 100644 --- a/models/ib/underlying.rb +++ b/models/ib/underlying.rb @@ -17,7 +17,7 @@ def default_attributes # Serialize under_comp parameters def serialize - [true, con_id, delta, price] + [con_id, delta, price] end # Comparison From 5e00e05302af566e47216573ac4df8621024f862 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sun, 5 May 2024 20:27:22 +0200 Subject: [PATCH 34/76] make known_servers a contant to be callable in message generation --- lib/server_versions.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/server_versions.rb b/lib/server_versions.rb index 33f9c04..044c30d 100644 --- a/lib/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, @@ -141,6 +141,6 @@ # 100 = enhanced handshake, msg length prefixes MIN_CLIENT_VER = 100 -#MAX_CLIENT_VER = 165 #known_servers[:min_server_ver_d_peg_orders] -MAX_CLIENT_VER = known_servers[:min_server_ver_historical_schedule] +#MAX_CLIENT_VER = 165 #KNOWN_SERVERS[:min_server_ver_d_peg_orders] +MAX_CLIENT_VER = KNOWN_SERVERS[:min_server_ver_historical_schedule] # imessages/outgoing/request_tick_Data is prepared for change to ver. 140 , its commented for now From a22301e4b82e31a0588e23406cc26ba0ccccaa46 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sun, 5 May 2024 20:57:32 +0200 Subject: [PATCH 35/76] apply latest place_order api structure from python code --- lib/ib/messages/outgoing/place_order.rb | 563 ++++++++++++++++-------- models/ib/order.rb | 26 +- 2 files changed, 387 insertions(+), 202 deletions(-) diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 4f74cdf..5d84e3b 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -3,205 +3,386 @@ module Messages module Outgoing extend Messages # def_message macros - # Data format is { :id => int: local_id, - # :contract => Contract, - # :order => Order } - PlaceOrder = def_message [ 3,0 ] ## ServerVersion > 145 && < 163: def_message[ 3,45 ] - ## server-version is not known at compilation time - ## Method call has to be replaced then - ## Max-Client_ver --> 144!! + PlaceOrder = def_message [3] ## ServerVersion > 145 && < 163: def_message[ 3,45 ] + # ## server-version is not known at compilation time + # ## Method call has to be replaced then + # ## Max-Client_ver --> 144!! class PlaceOrder def encode -# server_version = Connection.current.server_version + server_version = Connection.current.server_version + requested_version = server_version < KNOWN_SERVERS[:min_server_ver_not_held] ? 27 : 45 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.to_d, - 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 || "", + + error 'contract has to be specified' unless contract.is_a? IB::Contract + + # send place order msg + fields = [3] + fields.push(requested_version) if server_version < KNOWN_SERVERS[:min_server_ver_order_container] + fields.push(@data[:local_id]) + + # send contract fields + if server_version >= KNOWN_SERVERS[:min_server_ver_place_order_conid] + fields.push(contract.con_id) + end + + fields += [ + contract.symbol, + contract[:sec_type], + contract.expiry, + contract.strike.is_a?(Numeric) && contract.strike.positive? ? contract.strike : contract.strike.negative? ? 0 : '', + contract[:right], + contract.multiplier, + contract.exchange, + contract.primary_exchange, + contract.currency, + contract.local_symbol + ] + + if server_version >= KNOWN_SERVERS[:min_server_ver_trading_class] + fields.push(contract.trading_class) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_sec_id_type] + fields += [ + contract.sec_id_type, + contract.sec_id + ] + end + + # send main order fields + fields.push(if order.side == :short + 'SSHORT' + else + order.side == :short_exempt ? 'SSHORTX' : order.side.to_sup + end) + if server_version >= KNOWN_SERVERS[:min_server_ver_fractional_positions] + fields.push(order.total_quantity.to_d) + else + fields.push(order.total_quantity.to_i) + end + fields.push(order[:order_type]) # Internal code, 'LMT' instead of :limit + if server_version < KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] + fields.push(order.limit_price || 0) + else + fields.push(order.limit_price || '') + end + if server_version < KNOWN_SERVERS[:min_server_ver_trailing_percent] + fields.push(order.aux_price || 0) + else + fields.push(order.aux_price || '') + + # extended order fields + fields += [ + 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, # srv v4 and above + order.block_order || false, # srv v5 and above + order.sweep_to_fill || false, # srv v5 and above + order.display_size, # srv v5 and above + order[:trigger_method], # srv v5 and above + order.outside_rth || false, # was: ignore_rth # srv v5 and above + order.hidden || false + ] # srv v7 and above + end + + # Send combo legs for BAG requests (srv v8 and above) + if contract.bag? + fields.push(combo_legs.size) + fields += combo_legs.map do |the_leg| + array = [ + the_leg.con_id, + the_leg.ratio, + the_leg.side.to_sup, + the_leg.exchange, + the_leg[:open_close], + the_leg[:short_sale_slot], + the_leg.designated_location, + ] + array.push(the_leg.exempt_code) if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] + array + end.flatten + + # TODO: order_combo_leg? + if server_version >= KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] + fields.push(contract.combo_legs.size) + fields += contract.combo_legs.map { |leg| leg.price || '' } + end + + # TODO: smartComboRoutingParams + if server_version >= KNOWN_SERVERS[:min_server_ver_smart_combo_routing_params] + fields.push(order.combo_params.size) + fields += order.combo_params.to_a + end + end + + fields += [ + '', # send deprecated sharesAllocation field + order.discretionary_amount, + order.good_after_time, + order.good_till_date, + order.fa_group, + order.fa_method, + order.fa_percentage + ] + if server_version < KNOWN_SERVERS[:min_server_ver_fa_profile_desupport] + fields.push('') # send deprecated faProfile field + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] + fields.push(order.model_code || '') + end + + fields += [ + 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) + ] + + 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 || 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 + '', ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here + order[:auction_strategy], # AUCTION_MATCH, AUCTION_IMPROVEMENT, AUCTION_TRANSPARENT + order.starting_price || '', + order.stock_ref_price || '', + order.delta || '', + order.stock_range_lower || '', + order.stock_range_upper || '', + order.override_percentage_constraints || false, + order.volatility || '', + order.volatility ? order[:volatility_type] || 2 : '', + order[:delta_neutral_order_type], + order.delta_neutral_aux_price || '' + ] + + if order.delta_neutral_order_type && order.delta_neutral_order_type != :none + if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral_conid] + fields += [ + order.delta_neutral_con_id, + order.delta_neutral_settling_firm, + order.delta_neutral_clearing_account, + order[:delta_neutral_clearing_intent] + ] + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral_open_close] + fields += [ + order.delta_neutral_open_close, + order.delta_neutral_short_sale, + order.delta_neutral_short_sale_slot, + order.delta_neutral_designated_location + ] + end + end + + fields += [ + order.continuous_update, + order[:reference_price_type] || '', + order.trail_stop_price || '' + + ] + + fields.push(order.trailing_percent || '') if server_version >= KNOWN_SERVERS[:min_server_ver_trailing_percent] + + fields += if server_version >= KNOWN_SERVERS[:min_server_ver_scale_orders2] + [ + order.scale_init_level_size || '', + order.scale_subs_level_size || '' + ] + else + [ + '', + order.scale_init_level_size || '' + ] + end + + fields.push(order.scale_price_increment || '') + + if server_version >= KNOWN_SERVERS[:min_server_ver_scale_orders3] && order.scale_price_increment && + order.scale_price_increment > 0 + fields += [ + 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_init_position || '', + order.scale_init_fill_qty || '', order.scale_random_percent # default: false, + ] + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_scale_table] + fields += [ + order.scale_table, + order.active_start_time, + order.active_stop_time + ] + end + if server_version >= KNOWN_SERVERS[:min_server_ver_hedge_orders] + fields.push(order.hedge_type) + fields += order.hedge_param if order.hedge_param + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_opt_out_smart_routing] + fields.push(order.opt_out_smart_routing) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_pta_orders] + fields += [ + order.clearing_account, + order.clearing_intent + ] + end + + fields.push(order.not_held) if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] + + if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral] + fields += contract.serialize_under_comp + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_algo_orders] + fields += order.serialize_algo + end + if server_version >= KNOWN_SERVERS[:min_server_ver_algo_id] + fields.push(order.algo_id) + end + + fields.push(order.what_if) + fields.push(order.serialize_misc_options) if server_version >= KNOWN_SERVERS[:min_server_ver_linking] + fields.push(order.solicided) if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] + if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] + fields += [ + order.random_size, + order.random_price + ] + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_pegged_to_benchmark] + if order[:type] == 'PEG BENCH' + fields += [ + order.reference_contract_id, + order.is_pegged_change_amount_decrease, + order.pegged_change_amount, + order.reference_change_amount, + order.reference_exchange_id ] - 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)) -# + end - end - end # PlaceOrder + 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 + ] + end + + fields.push(order.ext_operator) if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] + + if server_version >= KNOWN_SERVERS[:min_server_ver_soft_dollar_tier] + fields += [ + order.soft_dollar_tier_name, + order.soft_dollar_tier_value + ] + end + + fields.push(order.cash_qty) if server_version >= KNOWN_SERVERS[:min_server_ver_cash_qty] + + if server_version >= KNOWN_SERVERS[:min_server_ver_decision_maker] + fields += [order.mifid_2_decision_maker, order.mifid_2_decision_algo] + end + if server_version >= KNOWN_SERVERS[:min_server_ver_mifid_execution] + fields += [order.mifid_2_execution_maker, order.mifid_2_execution_algo] + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_auto_price_for_hedge] + 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] + if server_version >= KNOWN_SERVERS[:min_server_ver_d_peg_orders] + fields.push(order.discretionary_up_to_limit_price) + end - end # module Outgoing - end # module Messages -end # module IB + if server_version >= KNOWN_SERVERS[:min_server_ver_price_mgmt_algo] + if order.use_price_management_algo.nil? + fields.push('') + else + fields.push(order.use_price_management_algo) + end + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_duration] + fields.push(order.duration) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_post_to_ats] + fields.push(order.post_to_ats) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_auto_cancel_parent] + fields.push(order.auto_cancel_parent) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_advanced_order_reject] + fields.push(order.advanced_order_reject) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_manual_order_time] + fields.push(order.manual_order_time) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_pegbest_pegmid_offsets] + send_mid_offsets = false + + fields.push(order.min_trade_qty) if contract.exchange == 'IBKRATS' + if ['PEG BEST', 'PEGBEST'].include?(order.type) + fields += [ + order.min_compete_size, + order.compete_against_best_offset + ] + send_mid_offsets = true if order.compete_against_best_offset.nil? # TODO: float max? + elsif ["PEG BEST", "PEGBEST"].include?(order.type) + send_mid_offsets = true + end + + if send_mid_offsets + fields += [ + order.mid_offset_at_whole, + order.mid_offset_at_half + ] + end + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_customer_account] + fields.push(order.customer_account) + end + + if server_version >= KNOWN_SERVERS[:min_server_ver_professional_customer] + fields.push(order.professional_account) + end + + fields + end + end + end + end +end diff --git a/models/ib/order.rb b/models/ib/order.rb index cbe2f72..0d27685 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -244,7 +244,16 @@ class Order < IB::Base :duration ,# => :int, :post_to_ats ,# => :int, :auto_cancel_parent, # => :bool - :is_O_ms_container + :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 @@ -421,21 +430,16 @@ def default_attributes # default valus are taken from order.java =end def serialize_conditions if conditions.empty? - 0 + [conditions.size] else - [ conditions.count ] + conditions.map( &:serialize )+ [ conditions_ignore_rth, conditions_cancel_order] + [conditions.size] + 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 - else - [algo_strategy, - algo_params.size, - algo_params.to_a, - algo_id ] # Vers 71 - end + return [''] if algo_strategy.blank? + + [algo_strategy, algo_params.size] + algo_params.to_a end # def serialize_soft_dollar_tier From 46858335ee568ce5a427fded779b2da878eacd7d Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 14 Jun 2024 10:20:48 +0200 Subject: [PATCH 36/76] Tests for managed accounts, verify and auto-adjust plugins --- plugins/ib/advanced-account.rb | 83 ++++++++++--------- plugins/ib/auto-adjust.rb | 14 ++-- plugins/ib/eod.rb | 26 +++++- plugins/ib/managed-accounts.rb | 18 ++-- plugins/ib/order-prototypes.rb | 20 +++-- plugins/ib/process-orders.rb | 69 +++++---------- plugins/ib/verify.rb | 28 ++++++- spec/ib/plugins/auto_adjust_spec.rb | 61 ++++++++++++++ spec/ib/plugins/managed_account_spec.rb | 36 ++++++++ .../verify_spec.rb} | 21 ++--- spec/main_helper.rb | 15 +++- 11 files changed, 265 insertions(+), 126 deletions(-) create mode 100644 spec/ib/plugins/auto_adjust_spec.rb create mode 100644 spec/ib/plugins/managed_account_spec.rb rename spec/ib/{plugins_spec.rb => plugins/verify_spec.rb} (66%) diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 0961333..bac21ee 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -1,23 +1,25 @@ 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 account_values.is_a? Array 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 - - else # not tested!! - if search_currency.present? - account_values.where( ['key like %', search_key] ).where( currency: search_currency ) - else # any currency - account_values.where( ['key like %', search_key] ) - end - end end @@ -26,7 +28,7 @@ def account_data_scan search_key, search_currency=nil 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 Orderrecord is returned. +the last associated Order-record is returned. Thus if several Orders are placed with the same order_ref, the active one is returned @@ -47,9 +49,9 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ 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 } + 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 } + matched_items = matched_items.find_all{|o| o.contract.con_id == con_id } end if status.present? @@ -96,7 +98,7 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ => 67 # returns local_id order.contract # updated contract-record - => #9534669, + => #9534669, :exchange=>"SGX", :right=>"", :include_expired=>false}> @@ -112,8 +114,8 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ # 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 - # adjust the orderprice to min-tick + def place_order order:, contract: nil, auto_adjust: true, convert_size: true + # adjust the order price to min-tick result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } #·IB::Symbols are always qualified. They carry a description-field qualified_contract = ->(c) { c.is_a?(IB::Contract) && ( c.description.present? || !c.con_id.to_i.zero? || (c.con_id.to_i <0 && c.sec_type == :bag )) } @@ -143,7 +145,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true if [ 110, # The price does not confirm to the minimum price variation for this contract 201, # Order rejected, No Trading permissions 203, # Security is not allowed for trading - 325, # Disretionary Orders are not supported for ths combination of oerder-type and exchange + 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. @@ -201,7 +203,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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 modificable. + It is checked, whether the order is still modifiable. Then the Order ist provided through the block. Any modification is done there. Important: The Block has to return the modified IB::Order @@ -210,7 +212,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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 manualy in the provided block +This has to be done manually in the provided block =end @@ -233,7 +235,7 @@ def modify_order local_id: nil, order_ref: nil, order:nil # # Submits a "WhatIf" Order # - # Returns the order_state.forcast + # Returns the order_state.forecast # # The order received from the TWS is kept in account.orders # @@ -244,13 +246,13 @@ def preview order:, contract: nil, **args_which_are_ignored q = Queue.new ib = IB::Connection.current the_local_id = nil + # put the order into the queue (and exit) if the event is fired req = ib.subscribe( :OpenOrder ){|m| q << m.order if m.order.local_id.to_i == the_local_id.to_i } - result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } order.what_if = true order.account = account the_local_id = order.place contract - Thread.new{ sleep 2 ; q.close } + 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 @@ -259,18 +261,18 @@ def preview order:, contract: nil, **args_which_are_ignored returned_order.order_state.forcast # return_value end -# closes the contract by submitting an appropiate order +# 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 opposide position is established. + # if _reverse_ is specified, the opposite position is established. # Any value in total_quantity is overwritten # # returns the order transmitted # - # raises an IB::Error if no PortfolioValues have been loaded to the IB::Acoount + # 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 @@ -282,8 +284,8 @@ def close order:, contract: nil, reverse: false, **args_which_are_ignored end end - contract &.verify{|c| order.contract = c} # if contract is specified: don't touch the parameter, get a new object . - error "Cannot transmit the order – No Contract given " unless order.contract.is_a?(IB::Contract) + order.contract = contract.verify.first unless contract.nil? + error "Cannot transmit the order – No Contract given " unless order.contract.is_a?( IB::Contract ) the_quantity = if reverse -contract_size[order.contract] * 2 @@ -305,25 +307,30 @@ def close order:, contract: nil, reverse: false, **args_which_are_ignored # just a wrapper to the Gateway-cancel-order method def cancel order: - Gateway.current.cancel_order order + Connection.current.cancel_order order end + ## ToDo ... needs adaption ! #returns an hash where portfolio_positions are grouped into Watchlists. # # Watchlist => [ contract => [ portfoliopositon] , ... ] ] # - def organize_portfolio_positions the_watchlists= IB::Gateway.current.active_watchlists + 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 | - z= the_watchlists.map do | w | - ref_con_id = pw.contract.con_id - watchlist_contract = w.find do |c| - c.is_a?(IB::Bag) ? c.combo_legs.map(&:con_id).include?(ref_con_id) : c.con_id == ref_con_id - end rescue nil - watchlist_contract.present? ? [w,watchlist_contract] : nil - end.compact - - z.empty? ? [ IB::Symbols::Unspecified, pw.contract, pw ] : z.first << pw + 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 diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb index df5e682..b9556b4 100644 --- a/plugins/ib/auto-adjust.rb +++ b/plugins/ib/auto-adjust.rb @@ -14,16 +14,17 @@ module AutoAdjust # # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit. # + # ATTENTION: The method mutates the Order-Object. + # # | min-tick | round | # |--------------|------------| # | 10 | 110 | # | 1 | 111 | # | 0.1 | 111.1 | - # | 0.001 | 111.11 | - # | 0.0001 | 111.11 | - # | 0.00001 | 111.111 | - # - |--------------|------------| + # | 0.01 | 111.11 | + # | 0.001 | 111.111 | + # | 0.0001 | 111.1111 | + # |--------------|------------| # def auto_adjust # lambda to perform the calculation @@ -40,13 +41,14 @@ def auto_adjust unless contract.is_a? IB::Bag # ensure that contract_details are present - min_tick = contract.verify.first.contract_detail.min_tick + 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_f, min_tick) unless limit_price.to_f.zero? self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? end end + end class Order include AutoAdjust end # class Order diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb index 0f118cd..dfed926 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -2,14 +2,32 @@ module IB require 'active_support/core_ext/date/calculations' require 'csv' - =begin Plugin to support EndOfDay OHLC-Data for a contract -Provides Contract.eod for EndOfDay historical data, - Contract.get_bars for custom ohlc-timeframes, - Contract.from_csv and Contract.to_csv to store and retrieve ohlc-data +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 diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index 2dde29b..fbac761 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -4,17 +4,20 @@ module IB Plugin for Managed Accounts -Provides `clients` and `advisor` methods that contain account-specific data +Provides `clients` and `advisor` objects (Type: IB::Account) that contain account-specific data. -* InitializeManagedAccounts +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 + +* provides * client.account_values * client.portfolio_values * client.contracts @@ -22,15 +25,20 @@ module IB 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 = Connection.new connect: false do | c | c.activate_plugin 'managed-accounts' - c.initialize_managed_accounts - c.get_account_data + c.initialize_managed_accounts # connects to the tws + c.get_account_data # populates c.clients end + account = ib.clients.first + puts account.portfolio_values.as_table + =end module ManagedAccounts diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order-prototypes.rb index c1d06a8..c627720 100644 --- a/plugins/ib/order-prototypes.rb +++ b/plugins/ib/order-prototypes.rb @@ -1,12 +1,20 @@ module IB -=begin - # Plugin to build IB::Order objects through singletons - # Limit.order size: 4, price: 10 - # Market.order: size: 4 - # etc - # +=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 diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb index 2de86fe..8e575db 100644 --- a/plugins/ib/process-orders.rb +++ b/plugins/ib/process-orders.rb @@ -1,5 +1,23 @@ - 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 =begin UpdateOrderDependingObject @@ -130,53 +148,4 @@ class Connection end ## module IB - -module IB - - class Order - # Auto Adjust implements a simple algorithm to ensure that an order is accepted - - # It reads `contract_detail.min_tick`. - # # - # If min_tick < 0.01, the real tick-increments differ fron the min_tick_value - # - # For J36 (jardines) min tick is 0.001, but the minimal increment is 0.005 - # For Tui1 its the samme, min_tick is 0.00001 , minimal increment ist 0.00005 - # - # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit. - # - # | min-tick | round | - # |--------------|------------| - # | 10 | 110 | - # | 1 | 111 | - # | 0.1 | 111.1 | - # | 0.001 | 111.11 | - # | 0.0001 | 111.11 | - # | 0.00001 | 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 - # ensure that contract_details are present - - min_tick = contract.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_f, min_tick) unless limit_price.to_f.zero? - self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? - end - end - end # class Order end # module diff --git a/plugins/ib/verify.rb b/plugins/ib/verify.rb index 470a947..695f290 100644 --- a/plugins/ib/verify.rb +++ b/plugins/ib/verify.rb @@ -1,4 +1,30 @@ 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 @@ -10,7 +36,7 @@ module Verify # # # The method accepts a block. The queried contract-Object is accessible there. - # If multiple contracts are specified, the block is executed with each of these contracts. + # 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. diff --git a/spec/ib/plugins/auto_adjust_spec.rb b/spec/ib/plugins/auto_adjust_spec.rb new file mode 100644 index 0000000..8cdf626 --- /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 + 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..2c18e34 --- /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_spec.rb b/spec/ib/plugins/verify_spec.rb similarity index 66% rename from spec/ib/plugins_spec.rb rename to spec/ib/plugins/verify_spec.rb index e0c44a5..456730a 100644 --- a/spec/ib/plugins_spec.rb +++ b/spec/ib/plugins/verify_spec.rb @@ -9,21 +9,20 @@ it{ expect( IB::Connection.current ).to be_a IB::Connection } end - context "Plugin not present" do - Given( :current ){ IB::Connection.current } - Then { current.plugins == [] } - Then { expect{ current.activate_plugin('invalid') }.to raise_error IB::Error } - - 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 "Prior to the activation of the verify plugin" do + it "Raises NoMethodError if the verify plugin is not activated" do expect{ stock.verify }.to raise_error NoMethodError end - it " Activated Verify Plugin " do + it "Gets the ConId if the Contact after the Verify Plugin is activated" do current = IB::Connection.current status = current.activate_plugin('verify') @@ -35,10 +34,6 @@ expect( complete_stock.con_id).to be > 0 end end - - - - - end +end diff --git a/spec/main_helper.rb b/spec/main_helper.rb index 8e8d3b3..e6bda1a 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -35,9 +35,18 @@ def should_not_log *patterns ## Connection helpers -def establish_connection - - ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) +def establish_connection *plugins + + if plugins.include? "managed-accounts" + OPTS[:connection].merge connect: false + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) do |c| + c.activate_plugin 'managed-accounts' + c.initialize_managed_accounts + c.get_account_data + end + else + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) + end if ib ib.wait_for :ManagedAccounts, 5 From b24ab240746dd108906b355a15b42c8e2125861f Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 18 Jun 2024 15:54:21 +0200 Subject: [PATCH 37/76] Adaption of OpenOrder to V10, incl. tests --- bin/simple | 91 +++++ lib/ib/connection.rb | 24 +- lib/ib/messages/incoming/abstract_message.rb | 2 +- lib/ib/messages/incoming/open_order.rb | 354 +++++++++--------- lib/ib/support.rb | 51 +-- lib/server_versions.rb | 3 +- models/ib/order.rb | 52 +-- models/ib/order_state.rb | 27 +- plugins/ib/auto-adjust.rb | 34 +- plugins/ib/connection-tools.rb | 32 +- .../messages/incoming/open_position_spec.rb | 211 +++++++++++ 11 files changed, 624 insertions(+), 257 deletions(-) create mode 100755 bin/simple create mode 100644 spec/ib/messages/incoming/open_position_spec.rb diff --git a/bin/simple b/bin/simple new file mode 100755 index 0000000..3253c19 --- /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_port = ARGV[0] || 'Gateway' + port = case specified_port + 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, port: port, connect: false 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.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 on Port #{port}, 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/lib/ib/connection.rb b/lib/ib/connection.rb index bcba9a7..6cbac67 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -27,12 +27,14 @@ class Connection attr_accessor :client_id attr_accessor :server_version attr_accessor :client_version + attr_accessor :host + 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 + def initialize host: '127.0.0.1:4002', + port: nil, # 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 @@ -68,13 +70,13 @@ def initialize host: '127.0.0.1', end @connected = false - self.next_local_id = nil + @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}." } + @next_local_id = msg.local_id + self.logger.info { "Got next valid order id: #{@next_local_id}." } end # # this block is executed before tws-communication is established @@ -83,7 +85,7 @@ def initialize host: '127.0.0.1', if connect update_next_order_id - Kernel.exit if self.next_local_id.nil? # emergency exit. + Kernel.exit if @next_local_id.nil? # emergency exit. # update_next_order_id should have raised an error end Connection.current = self @@ -104,14 +106,14 @@ def update_next_order_id send_message :RequestIds end 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 @@ -360,11 +362,11 @@ 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 "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 end diff --git a/lib/ib/messages/incoming/abstract_message.rb b/lib/ib/messages/incoming/abstract_message.rb index 0f29213..891cdbc 100644 --- a/lib/ib/messages/incoming/abstract_message.rb +++ b/lib/ib/messages/incoming/abstract_message.rb @@ -35,7 +35,7 @@ def initialize source @buffer = source ### DEBUG DEBUG DEBUG RAW STREAM ############### # if uncommented, the raw-input from the tws is included in the logging - ## puts "BUFFER :> #{buffer.inspect} " +## puts "BUFFER :> \n #{buffer.inspect} \n" ### DEBUG DEBUG DEBUG RAW STREAM ############### @data = Hash.new self.load diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 7e4c3e1..96b318a 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -5,65 +5,66 @@ module Incoming # OpenOrder is the longest message with complex processing logics OpenOrder = def_message [5, 0], # updated to v. 34 according to python (decoder.py processOpenOrder) - [ :order, :local_id, :int], + [ :order, :local_id, :int], - [ :contract, :contract], # read standard-contract + [ :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], + # 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 @@ -104,16 +105,14 @@ def order 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])) + :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 @@ -136,121 +135,128 @@ def load [ :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 ] ], - #AdjustedOrderParams - [ :order, :adjusted_order_type, :string ], - [ :order, :trigger_price, :decimal ], - [ :order, :trail_stop_price, :decimal ], # cpp -source: 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, :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, :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, :string ], - [ :order, :discretionary_up_to_limit_price, :string], - [ :order, :use_price_management_algo, :string], - [ :order, :duration, :int ] - [ :order, :post_to_ats, :int ] - [ :order, :auto_cancel_parent, :string] + [ :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 ], diff --git a/lib/ib/support.rb b/lib/ib/support.rb index d912526..e6ce097 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -18,7 +18,8 @@ def zero? 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 + i.is_a?( Integer ) && i != 2147483647 ? i : nil + end def read_float @@ -134,48 +135,48 @@ def read_array hashmode:false, &block 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.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] + 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_int, + 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 @@ -187,7 +188,7 @@ def tws nil.tws else self.flatten.map( &:tws ).join # [ "", [] , nil].flatten -> ["", nil] - # elemets with empty array's are cut + # elements with empty array's are cut # this is the desired behavior! end end diff --git a/lib/server_versions.rb b/lib/server_versions.rb index 044c30d..5d341ce 100644 --- a/lib/server_versions.rb +++ b/lib/server_versions.rb @@ -141,6 +141,5 @@ # 100 = enhanced handshake, msg length prefixes MIN_CLIENT_VER = 100 -#MAX_CLIENT_VER = 165 #KNOWN_SERVERS[:min_server_ver_d_peg_orders] -MAX_CLIENT_VER = KNOWN_SERVERS[:min_server_ver_historical_schedule] +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/models/ib/order.rb b/models/ib/order.rb index 0d27685..e3effd2 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -11,9 +11,9 @@ class Order < IB::Base # 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. @@ -28,9 +28,10 @@ class Order < IB::Base :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 @@ -77,8 +78,8 @@ class Order < IB::Base # 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, @@ -232,8 +233,7 @@ class Order < IB::Base # 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 + :cash_qty, # decimal : The native cash quantity :mifid_2_decision_maker, :mifid_2_decision_algo, :mifid_2_execution_maker, @@ -259,29 +259,29 @@ class Order < IB::Base # 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 + :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 :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 + [:side, :action] => PROPS[:side] # String: Action/side: BUY/SELL/SSHORT/SSHORTX prop :placed_at, :modified_at, @@ -381,7 +381,7 @@ def default_attributes # default valus are taken from order.java :conditions => [], :continuous_update => 0, :designated_location => '', # order.java # 487 - :display_size => 0, + :display_size => nil, :discretionary_amount => 0, :etrade_only => true, # stolen from python client :exempt_code => -1, diff --git a/models/ib/order_state.rb b/models/ib/order_state.rb index bd56a43..ef2bb70 100644 --- a/models/ib/order_state.rb +++ b/models/ib/order_state.rb @@ -9,12 +9,15 @@ class OrderState < IB::Base 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. @@ -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,13 +140,13 @@ 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 , + { :init_margin => init_margin_after, + :maint_margin => maint_margin_after_after, + :equity_with_loan => equity_with_loan_after , :commission => commission, :commission_currency=> commission_currency, :warning => warning_text } diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb index b9556b4..e9c4959 100644 --- a/plugins/ib/auto-adjust.rb +++ b/plugins/ib/auto-adjust.rb @@ -1,7 +1,33 @@ - module IB - module AutoAdjust +=begin + +Plugin that provides helper methods for orders + +Requires activation of the `verify`-Plugin + +Extends IB::Order + +Changes the IB::Order-object + +Public API +========== + +* auto_adjust + +Standard usage + +```ruby +c = IB::Stock.new symbol = 'GE' +o = IB::Limit.order contract: c, price: 150.0998, size: 100 +o.auto_adjust + +o.limit_price => 151 + +``` +=end + + module AutoAdjust # Auto Adjust implements a simple algorithm to ensure that an order is accepted @@ -23,7 +49,7 @@ module AutoAdjust # | 0.1 | 111.1 | # | 0.01 | 111.11 | # | 0.001 | 111.111 | - # | 0.0001 | 111.1111 | + # | 0.0001 | 111.111 | # |--------------|------------| # def auto_adjust @@ -43,7 +69,7 @@ def auto_adjust 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 + # 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_f, min_tick) unless limit_price.to_f.zero? self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? end diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index bbc8435..761925b 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -3,7 +3,15 @@ module IB =begin Plugin for advanced Connections -Provides `check_connection` and `safe_connect` +Public API +========== + +Extends IB::Connection + +Provides + * IB::Connection.current.check_connection + * IB::Connection.current.safe_connect + * IB::Connection.reconect =end @@ -51,7 +59,7 @@ def check_connection # Alternative to `Connection#connect'. # - # Trys to connect to the api. If the connection could not be established, waits + # 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. @@ -89,7 +97,27 @@ def safe_connect maximal_count_of_retry=100 end # def end + module ReConnect + def safe_reconnect + used_plugins = current.plugins + used_client_id = current.client_id + used_received = if current.received.nil? || current.received.empty? + false + else + true + end + current.disconnect + current = nil + c = Connection.new client_id: used_client_id + + + end + + end + class Connection include ConnectionTools + extend Reconnect end + end 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..d7f1957 --- /dev/null +++ b/spec/ib/messages/incoming/open_position_spec.rb @@ -0,0 +1,211 @@ +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 false } + its( :hidden ) { is_expected.to be false } + 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 } ### todo : empty?? + its( :percent_offset ) { is_expected.to be_nil } ### todo : zero?? + 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_nil } + its( :delta ) { is_expected.to be_nil } + its( :stock_range_lower ) { is_expected.to be_nil } + its( :stock_range_upper ) { is_expected.to be_nil } + its( :display_size ) { is_expected.to be_nil } ### unset if MAX_INT is transmitted + its( :block_order ) { is_expected.to be false } + its( :sweep_to_fill ) { is_expected.to be false } + its( :all_or_none ) { is_expected.to be false } + its( :min_quantity ) { is_expected.to be_nil } + its( :oca_type) { is_expected.to eq :reduce_no_block } # 3 + # etrade + its( :firm_quote_only ) { is_expected.to be false } + 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 be_nil } + 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_nil } + its( :trailing_percent ) { is_expected.to be_nil } + 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 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_nil } + 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 + From df647bca8892a1b0acd5611734777e2b358a8cf7 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 20 Jun 2024 17:41:21 +0200 Subject: [PATCH 38/76] Introducing server_version methods for models and messages, Improved versioning of PlaceOrder Message --- lib/ib/base.rb | 6 + lib/ib/constants.rb | 3 +- lib/ib/messages/abstract_message.rb | 4 + lib/ib/messages/incoming/open_order.rb | 26 +- lib/ib/messages/outgoing/abstract_message.rb | 21 +- lib/ib/messages/outgoing/place_order.rb | 295 ++++-------------- lib/ib/support.rb | 2 +- models/ib/order.rb | 210 +++++++++++-- .../messages/incoming/open_position_spec.rb | 3 +- 9 files changed, 275 insertions(+), 295 deletions(-) diff --git a/lib/ib/base.rb b/lib/ib/base.rb index fc99cec..e17cdfb 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 diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index 8296319..e11995e 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -202,7 +202,8 @@ module IB '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 diff --git a/lib/ib/messages/abstract_message.rb b/lib/ib/messages/abstract_message.rb index eb40897..f710f1f 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 diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index 96b318a..d32d4fd 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -199,22 +199,22 @@ def load [ :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, :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_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 ], diff --git a/lib/ib/messages/outgoing/abstract_message.rb b/lib/ib/messages/outgoing/abstract_message.rb index 8fa8ff8..e4b21cd 100644 --- a/lib/ib/messages/outgoing/abstract_message.rb +++ b/lib/ib/messages/outgoing/abstract_message.rb @@ -14,6 +14,8 @@ def initialize data={} @created_at = Time.now end + + # This causes the message to send itself over the server socket in server[:socket]. # "server" is the @server instance variable from the IB object. # You can also use this to e.g. get the server version number. @@ -36,7 +38,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. @@ -44,29 +46,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) + if self.class.properties?.include?(:request_id) @data[:request_id] = if @data[:request_id].blank? && @data[:ticker_id].blank? && @data[:id].blank? - rand(9999) + rand(9999) else - @data[:id] || @data[:ticker_id] || @data[:request_id] + @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 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/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 5d84e3b..220ede9 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -3,94 +3,20 @@ module Messages module Outgoing extend Messages # def_message macros - PlaceOrder = def_message [3] ## ServerVersion > 145 && < 163: def_message[ 3,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 - requested_version = server_version < KNOWN_SERVERS[:min_server_ver_not_held] ? 27 : 45 order = @data[:order] contract = @data[:contract] error 'contract has to be specified' unless contract.is_a? IB::Contract # send place order msg - fields = [3] - fields.push(requested_version) if server_version < KNOWN_SERVERS[:min_server_ver_order_container] - fields.push(@data[:local_id]) - - # send contract fields - if server_version >= KNOWN_SERVERS[:min_server_ver_place_order_conid] - fields.push(contract.con_id) - end - - fields += [ - contract.symbol, - contract[:sec_type], - contract.expiry, - contract.strike.is_a?(Numeric) && contract.strike.positive? ? contract.strike : contract.strike.negative? ? 0 : '', - contract[:right], - contract.multiplier, - contract.exchange, - contract.primary_exchange, - contract.currency, - contract.local_symbol - ] - - if server_version >= KNOWN_SERVERS[:min_server_ver_trading_class] - fields.push(contract.trading_class) - end - - if server_version >= KNOWN_SERVERS[:min_server_ver_sec_id_type] - fields += [ - contract.sec_id_type, - contract.sec_id - ] - end - - # send main order fields - fields.push(if order.side == :short - 'SSHORT' - else - order.side == :short_exempt ? 'SSHORTX' : order.side.to_sup - end) - if server_version >= KNOWN_SERVERS[:min_server_ver_fractional_positions] - fields.push(order.total_quantity.to_d) - else - fields.push(order.total_quantity.to_i) - end - fields.push(order[:order_type]) # Internal code, 'LMT' instead of :limit - if server_version < KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] - fields.push(order.limit_price || 0) - else - fields.push(order.limit_price || '') - end - if server_version < KNOWN_SERVERS[:min_server_ver_trailing_percent] - fields.push(order.aux_price || 0) - else - fields.push(order.aux_price || '') - - # extended order fields - fields += [ - 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, # srv v4 and above - order.block_order || false, # srv v5 and above - order.sweep_to_fill || false, # srv v5 and above - order.display_size, # srv v5 and above - order[:trigger_method], # srv v5 and above - order.outside_rth || false, # was: ignore_rth # srv v5 and above - order.hidden || false - ] # srv v7 and above - end + fields = [ super ] + fields << contract.serialize_short(:primary_exchange, :sec_id_type) + fields << order.serialize_main_order_fields + fields << order.serialize_extended_order_fields # Send combo legs for BAG requests (srv v8 and above) if contract.bag? @@ -105,42 +31,31 @@ def encode the_leg[:short_sale_slot], the_leg.designated_location, ] - array.push(the_leg.exempt_code) if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] + array.push(the_leg.exempt_code) if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] # 51 array end.flatten # TODO: order_combo_leg? - if server_version >= KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] + if server_version >= KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] # 61 fields.push(contract.combo_legs.size) fields += contract.combo_legs.map { |leg| leg.price || '' } end # TODO: smartComboRoutingParams - if server_version >= KNOWN_SERVERS[:min_server_ver_smart_combo_routing_params] + if server_version >= KNOWN_SERVERS[:min_server_ver_smart_combo_routing_params] # 57 fields.push(order.combo_params.size) fields += order.combo_params.to_a end end - fields += [ - '', # send deprecated sharesAllocation field - order.discretionary_amount, - order.good_after_time, - order.good_till_date, - order.fa_group, - order.fa_method, - order.fa_percentage - ] - if server_version < KNOWN_SERVERS[:min_server_ver_fa_profile_desupport] - fields.push('') # send deprecated faProfile field - end + fields << order.serialize_auxilery_order_fields # incluing advisory order fields if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] - fields.push(order.model_code || '') + fields.push(order.model_code ) end fields += [ - order[:short_sale_slot] || 0, # 0 only for retail, 1 or 2 for institution (Institutional) + 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) ] @@ -150,138 +65,74 @@ def encode fields += [ order[:rule_80a], # .to_sup[0..0], order.settling_firm, - order.all_or_none || false, - order.min_quantity || '', - order.percent_offset || '', + order.all_or_none, + 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 '', ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here order[:auction_strategy], # AUCTION_MATCH, AUCTION_IMPROVEMENT, AUCTION_TRANSPARENT - order.starting_price || '', - order.stock_ref_price || '', - order.delta || '', - order.stock_range_lower || '', - order.stock_range_upper || '', - order.override_percentage_constraints || false, - order.volatility || '', - order.volatility ? order[:volatility_type] || 2 : '', - order[:delta_neutral_order_type], - order.delta_neutral_aux_price || '' + order.starting_price , + order.stock_ref_price , + order.delta , + order.stock_range_lower , + order.stock_range_upper , + order.override_percentage_constraints, + order.serialize_volatility_order_fields, + order.serialize_delta_neutral_order_fields ] - if order.delta_neutral_order_type && order.delta_neutral_order_type != :none - if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral_conid] - fields += [ - order.delta_neutral_con_id, - order.delta_neutral_settling_firm, - order.delta_neutral_clearing_account, - order[:delta_neutral_clearing_intent] - ] - end - - if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral_open_close] - fields += [ - order.delta_neutral_open_close, - order.delta_neutral_short_sale, - order.delta_neutral_short_sale_slot, - order.delta_neutral_designated_location - ] - end - end - fields += [ order.continuous_update, - order[:reference_price_type] || '', - order.trail_stop_price || '' - + order[:reference_price_type] , + order.trail_stop_price, + order.trailing_percent ] - fields.push(order.trailing_percent || '') if server_version >= KNOWN_SERVERS[:min_server_ver_trailing_percent] - - fields += if server_version >= KNOWN_SERVERS[:min_server_ver_scale_orders2] - [ - order.scale_init_level_size || '', - order.scale_subs_level_size || '' - ] - else - [ - '', - order.scale_init_level_size || '' - ] - end - - fields.push(order.scale_price_increment || '') + fields << order.serialize_scale_order_fields - if server_version >= KNOWN_SERVERS[:min_server_ver_scale_orders3] && order.scale_price_increment && - order.scale_price_increment > 0 - fields += [ - 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, - ] - end + fields.push order.hedge_type + fields.push order.hedge_param # default is [] --> omitted if left default + fields.push order.opt_out_smart_routing - if server_version >= KNOWN_SERVERS[:min_server_ver_scale_table] - fields += [ - order.scale_table, - order.active_start_time, - order.active_stop_time - ] - end - if server_version >= KNOWN_SERVERS[:min_server_ver_hedge_orders] - fields.push(order.hedge_type) - fields += order.hedge_param if order.hedge_param - end + fields.push order.clearing_account + fields.push order.clearing_intent - if server_version >= KNOWN_SERVERS[:min_server_ver_opt_out_smart_routing] - fields.push(order.opt_out_smart_routing) - end + fields.push(order.not_held) if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] #44 - if server_version >= KNOWN_SERVERS[:min_server_ver_pta_orders] - fields += [ - order.clearing_account, - order.clearing_intent - ] - end - - fields.push(order.not_held) if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] - - if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral] + if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral] # 40 fields += contract.serialize_under_comp end - if server_version >= KNOWN_SERVERS[:min_server_ver_algo_orders] + if server_version >= KNOWN_SERVERS[:min_server_ver_algo_orders] # 41 fields += order.serialize_algo end - if server_version >= KNOWN_SERVERS[:min_server_ver_algo_id] + if server_version >= KNOWN_SERVERS[:min_server_ver_algo_id] # 71 fields.push(order.algo_id) end fields.push(order.what_if) - fields.push(order.serialize_misc_options) if server_version >= KNOWN_SERVERS[:min_server_ver_linking] - fields.push(order.solicided) if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] - if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] + fields.push(order.serialize_misc_options) if server_version >= KNOWN_SERVERS[:min_server_ver_linking] # 70 + fields.push(order.solicided) if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] # 73 + if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] # 76 fields += [ order.random_size, order.random_price ] end - if server_version >= KNOWN_SERVERS[:min_server_ver_pegged_to_benchmark] - if order[:type] == 'PEG BENCH' - fields += [ - order.reference_contract_id, - order.is_pegged_change_amount_decrease, - order.pegged_change_amount, - order.reference_change_amount, - order.reference_exchange_id - ] - end - + fields << order.serialize_pegged_order_fields +# if server_version >= KNOWN_SERVERS[:min_server_ver_pegged_to_benchmark] # 102 +# if order[:order_type] == 'PEG BENCH' +# fields += [ +# order.reference_contract_id, +# order.is_pegged_change_amount_decrease, +# order.pegged_change_amount, +# order.reference_change_amount, +# order.reference_exchange_id +# ] +# end +# fields += order.serialize_conditions fields += [ order.adjusted_order_type, @@ -292,25 +143,15 @@ def encode order.adjusted_trailing_amount, order.adjustable_trailing_unit ] - end +# end fields.push(order.ext_operator) if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] - if server_version >= KNOWN_SERVERS[:min_server_ver_soft_dollar_tier] - fields += [ - order.soft_dollar_tier_name, - order.soft_dollar_tier_value - ] - end + fields << order.serialize_soft_dollar_tier - fields.push(order.cash_qty) if server_version >= KNOWN_SERVERS[:min_server_ver_cash_qty] + fields.push(order.cash_qty) if server_version >= KNOWN_SERVERS[:min_server_ver_cash_qty] # 111 - if server_version >= KNOWN_SERVERS[:min_server_ver_decision_maker] - fields += [order.mifid_2_decision_maker, order.mifid_2_decision_algo] - end - if server_version >= KNOWN_SERVERS[:min_server_ver_mifid_execution] - fields += [order.mifid_2_execution_maker, order.mifid_2_execution_algo] - end + fields << order.serialize_mifid_order_fields if server_version >= KNOWN_SERVERS[:min_server_ver_auto_price_for_hedge] fields.push(order.dont_use_auto_price_for_hedge) @@ -323,11 +164,7 @@ def encode end if server_version >= KNOWN_SERVERS[:min_server_ver_price_mgmt_algo] - if order.use_price_management_algo.nil? - fields.push('') - else - fields.push(order.use_price_management_algo) - end + fields.push(order.use_price_management_algo) end if server_version >= KNOWN_SERVERS[:min_server_ver_duration] @@ -350,27 +187,7 @@ def encode fields.push(order.manual_order_time) end - if server_version >= KNOWN_SERVERS[:min_server_ver_pegbest_pegmid_offsets] - send_mid_offsets = false - - fields.push(order.min_trade_qty) if contract.exchange == 'IBKRATS' - if ['PEG BEST', 'PEGBEST'].include?(order.type) - fields += [ - order.min_compete_size, - order.compete_against_best_offset - ] - send_mid_offsets = true if order.compete_against_best_offset.nil? # TODO: float max? - elsif ["PEG BEST", "PEGBEST"].include?(order.type) - send_mid_offsets = true - end - - if send_mid_offsets - fields += [ - order.mid_offset_at_whole, - order.mid_offset_at_half - ] - end - end + fields << order.serialize_peg_best_and_mid if server_version >= KNOWN_SERVERS[:min_server_ver_customer_account] fields.push(order.customer_account) diff --git a/lib/ib/support.rb b/lib/ib/support.rb index e6ce097..9d0e324 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -135,7 +135,7 @@ def read_array hashmode:false, &block 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] diff --git a/models/ib/order.rb b/models/ib/order.rb index e3effd2..a3053af 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -106,7 +106,6 @@ class Order < IB::Base # 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: @@ -276,8 +275,6 @@ class Order < IB::Base # 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 :open_close => PROPS[:open_close], # Originally String: O=Open, C=Close () # for ComboLeg compatibility: SAME = 0; OPEN = 1; CLOSE = 2; UNKNOWN = 3; @@ -295,7 +292,7 @@ class Order < IB::Base # CondPriceMax, 62.0; -- max and min-price # CondPriceMin.;60.0 - + prop :etrade_only, :firm_quote_only, :nbbo_price_cap # prop :misc1, :misc2, :misc3, :misc4, :misc5, :misc6, :misc7, :misc8 # just 4 debugging alias order_combo_legs leg_prices @@ -371,56 +368,108 @@ def order_state= state def default_attributes # default valus are taken from order.java - # public Order() { } + # public Order() { } super.merge( :active_start_time => "", # order.java # 470 # Vers 69 - :active_stop_time => "", #order.java # 471 # Vers 69 + :active_stop_time => "", # order.java # 471 # Vers 69 + :algo_params => Hash.new, #{}, :algo_strategy => '', - :algo_id => '' , # order.java # 495 + :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 =>[], #{}, :conditions => [], :continuous_update => 0, + :delta => "", :designated_location => '', # order.java # 487 :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 + :hedge_param => [], + :hidden => 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_state => IB::OrderState.new( :status => 'New', + :filled => 0, + :remaining => 0, + :price => 0, + :average_price => 0 ), :origin => :customer, :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 + :reference_price_type => "", :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, + :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_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 @@ -430,26 +479,125 @@ def default_attributes # default valus are taken from order.java =end def serialize_conditions if conditions.empty? - [conditions.size] + [ 0 ] else - [conditions.size] + conditions.map(&:serialize) + [conditions_ignore_rth, conditions_cancel_order] + [ conditions.size ] + conditions.map( &:serialize ) + [ conditions_ignore_rth, conditions_cancel_order ] end end def serialize_algo return [''] if algo_strategy.blank? - [algo_strategy, algo_params.size] + algo_params.to_a end - # def serialize_soft_dollar_tier - # [soft_dollar_tier_params[:name],soft_dollar_tier_params[:val]] - # 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_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 + 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 + [ "do not include" ] + end + end + + def serialize_soft_dollar_tier + [ soft_dollar_tier_name, + soft_dollar_tier_value + ] + 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_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 diff --git a/spec/ib/messages/incoming/open_position_spec.rb b/spec/ib/messages/incoming/open_position_spec.rb index d7f1957..701ef6c 100644 --- a/spec/ib/messages/incoming/open_position_spec.rb +++ b/spec/ib/messages/incoming/open_position_spec.rb @@ -83,7 +83,8 @@ 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 Hash } + 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 } From d837d2c12a12663f5654e1a38a39bddd64ab0f01 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 20 Jun 2024 21:25:40 +0200 Subject: [PATCH 39/76] =?UTF-8?q?include=20plugin=20=C2=BBsymbols=C2=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/ib/symbols.rb | 116 +++++++++++++++++++++++++++ plugins/ib/symbols/abstract.rb | 136 ++++++++++++++++++++++++++++++++ plugins/ib/symbols/bonds.rb | 28 +++++++ plugins/ib/symbols/cfd.rb | 19 +++++ plugins/ib/symbols/combo.rb | 55 +++++++++++++ plugins/ib/symbols/commodity.rb | 17 ++++ plugins/ib/symbols/forex.rb | 41 ++++++++++ plugins/ib/symbols/futures.rb | 117 +++++++++++++++++++++++++++ plugins/ib/symbols/index.rb | 43 ++++++++++ plugins/ib/symbols/options.rb | 107 +++++++++++++++++++++++++ plugins/ib/symbols/stocks.rb | 38 +++++++++ 11 files changed, 717 insertions(+) create mode 100644 plugins/ib/symbols.rb create mode 100644 plugins/ib/symbols/abstract.rb create mode 100644 plugins/ib/symbols/bonds.rb create mode 100644 plugins/ib/symbols/cfd.rb create mode 100644 plugins/ib/symbols/combo.rb create mode 100644 plugins/ib/symbols/commodity.rb create mode 100644 plugins/ib/symbols/forex.rb create mode 100644 plugins/ib/symbols/futures.rb create mode 100644 plugins/ib/symbols/index.rb create mode 100644 plugins/ib/symbols/options.rb create mode 100644 plugins/ib/symbols/stocks.rb diff --git a/plugins/ib/symbols.rb b/plugins/ib/symbols.rb new file mode 100644 index 0000000..32098d6 --- /dev/null +++ b/plugins/ib/symbols.rb @@ -0,0 +1,116 @@ +=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" + [ :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..43f63e0 --- /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..304f636 --- /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..2b050bc --- /dev/null +++ b/plugins/ib/symbols/combo.rb @@ -0,0 +1,55 @@ +# 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 + + @contracts ||= { #super.merge( + stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: 5000, + expiry: IB::Option.next_expiry, trading_class: 'OESX' ) , + stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: 5000, back: '2m' , + front: IB::Option.next_expiry, trading_class: 'OESX' ), + stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: 4900 ), + front: 4500, back: 5300, + expiry: IB::Option.next_expiry + ), + stoxx_vertical: IB::Vertical.build( from: IB::Symbols::Index.stoxx, sell: 4500, buy: 5000, right: :put, + expiry: IB::Option.next_expiry, trading_class: 'OESX'), + zn_calendar: IB::Calendar.fabricate( IB::Symbols::Futures.zn, '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', 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' + ), + wti_coil: Bag.new( symbol: 'WTI', currency: 'USD', exchange: 'SMART', combo_legs: + [ ComboLeg.new( con_id: 55928698, action: :buy, exchange: 'IPE', ratio: 1), # WTI future June 2017 + ComboLeg.new( con_id: 55850663, action: :sell, exchange: 'IPE', ratio: 1 ) ], # COIL future June 2017 + description: 'Smart Future Spread WTI - COIL (June 2017) ' + ), + wti_brent: Bag.new( symbol: 'CL.BZ', currency: 'USD', exchange: 'NYMEX', combo_legs: + [ ComboLeg.new( con_id: 47207310, action: :buy, exchange: 'NYMEX', ratio: 1), # CL Dec'16 @NYMEX + ComboLeg.new( con_id: 47195961, action: :sell, exchange: 'NYMEX', ratio: 1 ) ], #BZ Dec'16 @NYMEX + description: ' WTI - Brent Spread (Dez. 2016)' + ) + } + # ) + end + + end + end +end diff --git a/plugins/ib/symbols/commodity.rb b/plugins/ib/symbols/commodity.rb new file mode 100644 index 0000000..b7b52ce --- /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..7d99502 --- /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..521fa22 --- /dev/null +++ b/plugins/ib/symbols/futures.rb @@ -0,0 +1,117 @@ +# 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 => "E-Mini Nasdaq 100 future"), + :micro_nq => IB::Future.new(:symbol => "MNQ", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 2, + :description => "E-Mini Nasdaq 100 future"), + :es => IB::Future.new(:symbol => "ES", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 50, + :description => "E-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 E-Mini S&P 500 future"), + :russell => IB::Future.new(:symbol => "RTY", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 5, + :description => "Micro E-Mini Russell 2000 future"), + :micro_russell => IB::Future.new(:symbol => "M2K", + :expiry => IB::Future.next_expiry, + :exchange => "CME", + :currency => "USD", + :multiplier => 5, + :description => "Micro E-Mini 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'), + :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 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..cac4348 --- /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" ), + :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..a3c12d3 --- /dev/null +++ b/plugins/ib/symbols/options.rb @@ -0,0 +1,107 @@ +# 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', + 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 Chain ( monthly expiry)'), + :ibm_lazy_expiry => IB::Option.new( symbol: 'IBM', + right: :put, + strike: 180, + exchange: 'SMART', + description: 'IBM-Option Chain with strike 140'), + :ibm_lazy_strike => IB::Option.new( symbol: 'IBM', + right: :put, + exchange: 'SMART', + expiry: IB::Option.next_expiry, + description: 'IBM-Option Chain ( monthly expiry)'), + + :goog100 => IB::Option.new( symbol: 'GOOG', + currency: 'USD', + strike: 100, + multiplier: 100, + right: :call, + exchange: 'SMART', + expiry: IB::Option.next_expiry, + description: 'Google Call Option with 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..e0aaec0 --- /dev/null +++ b/plugins/ib/symbols/stocks.rb @@ -0,0 +1,38 @@ +# 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 => IB::Stock.new( symbol: 'MSFT', primary_exchange: 'ISLAND', + description: 'Apple, primary trading @ ISLAND'), ## primary exchange set + :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: 'Siemes AG'), + :wrong => IB::Stock.new(:symbol => "QEEUUE", + :exchange => "NYSE", + :currency => "USD", + :description => "Non-existent stock") + ) + end + + end + end +end From 02b60829239bb2e29d335f9e9e501a1a5eef7be5 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 20 Jun 2024 22:12:42 +0200 Subject: [PATCH 40/76] Limit-order test with order-serializers --- models/ib/order.rb | 2 +- spec/ib/messages/outgoing/limit_order_spec.rb | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 spec/ib/messages/outgoing/limit_order_spec.rb diff --git a/models/ib/order.rb b/models/ib/order.rb index a3053af..0ef7856 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -557,7 +557,7 @@ def serialize_pegged_order_fields reference_change_amount, reference_exchange_id ] else - [ "do not include" ] + [] end end diff --git a/spec/ib/messages/outgoing/limit_order_spec.rb b/spec/ib/messages/outgoing/limit_order_spec.rb new file mode 100644 index 0000000..a4170db --- /dev/null +++ b/spec/ib/messages/outgoing/limit_order_spec.rb @@ -0,0 +1,72 @@ +require 'main_helper' + +RSpec.describe IB::Messages::Outgoing do + + before(:all) do + establish_connection + ib = IB::Connection.current + ib.activate_plugin 'order-prototypes' + ib.activate_plugin 'symbols' + + end + + context 'Stock' do + + Given( :soft ){ IB::Symbols::Stocks.msft } + When( :limit_order ){ IB::Limit.order size: 100, price: 200, contract: soft, account: ACCOUNT } + Then { limit_order.serialize_main_order_fields == [ "BUY", 100, "LMT", 200, ""] } + Then { limit_order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + Then { limit_order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { limit_order.serialize_conditions == [ 0 ] } + Then { limit_order.serialize_algo == [ "" ] } + Then { limit_order.serialize_volatility_order_fields == [ "", ""] } + Then { limit_order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { limit_order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { limit_order.serialize_pegged_order_fields == [] } + Then { limit_order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { limit_order.serialize_peg_best_and_mid == [] } + + end + +# +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# if subject.server_version < 177 +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields +# else +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields +## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields +# end +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +# +# end +# +# +end # describe IB::Messages:Outgoing From 32df99ad5d14ee56cdd291bfa9274e889ea42afe Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 21 Jun 2024 21:50:21 +0200 Subject: [PATCH 41/76] Tests on OrderPrototypes --- lib/ib/constants.rb | 1 + models/ib/order.rb | 8 +- plugins/ib/order_prototypes/limit.rb | 4 +- plugins/ib/order_prototypes/pegged.rb | 90 ++++++++++--------- plugins/ib/symbols/stocks.rb | 50 ++++++----- .../order-prototypes}/limit_order_spec.rb | 37 ++++---- .../pegged2bench_order_spec.rb | 54 +++++++++++ .../pegged2primary_order_spec.rb | 42 +++++++++ .../order-prototypes/stop_order_spec.rb | 40 +++++++++ .../order-prototypes/volatility_order_spec.rb | 43 +++++++++ 10 files changed, 285 insertions(+), 84 deletions(-) rename spec/ib/{messages/outgoing => plugins/order-prototypes}/limit_order_spec.rb (64%) create mode 100644 spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb create mode 100644 spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb create mode 100644 spec/ib/plugins/order-prototypes/stop_order_spec.rb create mode 100644 spec/ib/plugins/order-prototypes/volatility_order_spec.rb diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index e11995e..daf91a8 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -198,6 +198,7 @@ module IB '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 , 'BOX TOP' => :box_top, # Box Top 'PEG MKT' => :pegged_to_market, # Pegged-to-Market 'PEG STK' => :pegged_to_market, # Pegged-to-Stock diff --git a/models/ib/order.rb b/models/ib/order.rb index 0ef7856..568e243 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -202,7 +202,6 @@ class Order < IB::Base :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 , @@ -276,6 +275,7 @@ class Order < IB::Base # safety constraints, unless this parameter is set to True. :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 @@ -372,6 +372,7 @@ def default_attributes # default valus are taken from order.java super.merge( :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 @@ -390,6 +391,7 @@ def default_attributes # default valus are taken from order.java :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 => "", @@ -409,9 +411,13 @@ def default_attributes # default valus are taken from order.java :override_percentage_constraints => false, :percent_offset =>"", :parent_id => 0, + :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 diff --git a/plugins/ib/order_prototypes/limit.rb b/plugins/ib/order_prototypes/limit.rb index 4f226f6..a40610c 100644 --- a/plugins/ib/order_prototypes/limit.rb +++ b/plugins/ib/order_prototypes/limit.rb @@ -5,11 +5,11 @@ module Limit class << self def defaults - super.merge order_type: :limit + super.merge order_type: :limit end def aliases - super.merge limit_price: :price + super.merge limit_price: :price end def requirements diff --git a/plugins/ib/order_prototypes/pegged.rb b/plugins/ib/order_prototypes/pegged.rb index 8f9c31f..4636597 100644 --- a/plugins/ib/order_prototypes/pegged.rb +++ b/plugins/ib/order_prototypes/pegged.rb @@ -4,11 +4,12 @@ module Pegged2Primary class << self def defaults - super.merge order_type: 'REL' , tif: :day + super.merge order_type: :pegged_to_primary , tif: :day end def aliases - super.merge limit_price: :price_cap, aux_price: :offset_amount + super.merge limit_price: :price_cap, + aux_price: :offset_amount end def requirements @@ -85,7 +86,7 @@ module Pegged2Stock class << self def defaults - super.merge order_type: 'PEG STK' + super.merge order_type: 'PEG STK' end def aliases @@ -93,9 +94,9 @@ def aliases end def requirements - super.merge total_quantity: :decimal, - delta: 'required Delta of the Option', - starting_price: 'initial Limit-Price for the Option' + super.merge total_quantity: :decimal, + delta: 'required Delta of the Option', + starting_price: 'initial Limit-Price for the Option' end def optional @@ -130,44 +131,45 @@ module Pegged2Benchmark extend OrderPrototype class << self - def defaults - super.merge order_type: 'PEG BENCH' - end - - - - - - def requirements - super.merge total_quantity: :decimal, - delta: 'required Delta of the Option', - starting_price: 'initial Limit-Price for the Option' , - is_pegged_change_amount_decrease: 'increase(true) / decrease(false) Price', - pegged_change_amount: ' (increase/decrceas) 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', - reference_exchange: "Exchange of the reference contract" - - - - - - 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' - 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 + 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/symbols/stocks.rb b/plugins/ib/symbols/stocks.rb index e0aaec0..ead7157 100644 --- a/plugins/ib/symbols/stocks.rb +++ b/plugins/ib/symbols/stocks.rb @@ -7,29 +7,35 @@ module Stocks 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."), + :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 => IB::Stock.new( symbol: 'MSFT', primary_exchange: 'ISLAND', - description: 'Apple, primary trading @ ISLAND'), ## primary exchange set - :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: 'Siemes AG'), - :wrong => IB::Stock.new(:symbol => "QEEUUE", - :exchange => "NYSE", - :currency => "USD", - :description => "Non-existent stock") + :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 diff --git a/spec/ib/messages/outgoing/limit_order_spec.rb b/spec/ib/plugins/order-prototypes/limit_order_spec.rb similarity index 64% rename from spec/ib/messages/outgoing/limit_order_spec.rb rename to spec/ib/plugins/order-prototypes/limit_order_spec.rb index a4170db..2075ed4 100644 --- a/spec/ib/messages/outgoing/limit_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -1,6 +1,6 @@ require 'main_helper' -RSpec.describe IB::Messages::Outgoing do +RSpec.describe IB::Order do before(:all) do establish_connection @@ -10,21 +10,28 @@ end - context 'Stock' do + context 'Limit Order to buy 100 Microsoft shares at 200 $ a piece' do - Given( :soft ){ IB::Symbols::Stocks.msft } - When( :limit_order ){ IB::Limit.order size: 100, price: 200, contract: soft, account: ACCOUNT } - Then { limit_order.serialize_main_order_fields == [ "BUY", 100, "LMT", 200, ""] } - Then { limit_order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } - Then { limit_order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } - Then { limit_order.serialize_conditions == [ 0 ] } - Then { limit_order.serialize_algo == [ "" ] } - Then { limit_order.serialize_volatility_order_fields == [ "", ""] } - Then { limit_order.serialize_scale_order_fields == ["", "", "", "", "", ""] } - Then { limit_order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { limit_order.serialize_pegged_order_fields == [] } - Then { limit_order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { limit_order.serialize_peg_best_and_mid == [] } + 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 "Normal order conditions apply" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_volatility_order_fields == [ "", ""] } + Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { order.serialize_pegged_order_fields == [] } + Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { order.serialize_peg_best_and_mid == [] } + end end 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..c544eb6 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb @@ -0,0 +1,54 @@ +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 + + STRIKE = 2000 + + context 'Pegged Bench order on IBM using Microsoft 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 "Normal order conditions apply" do + Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { order.serialize_volatility_order_fields == [ "", ""] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { order.serialize_peg_best_and_mid == [] } + 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..6d1ecd5 --- /dev/null +++ b/spec/ib/plugins/order-prototypes/pegged2primary_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 + + STRIKE = 2000 + + context 'Pegged to Primary order on Microsoft 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 conditions apply" do + Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { order.serialize_volatility_order_fields == [ "", ""] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { order.serialize_pegged_order_fields == [] } + Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { order.serialize_peg_best_and_mid == [] } + 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..7006fe5 --- /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 on Microsoft' 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 orders" do + Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } + end + + context "Normal order conditions apply" do + Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_volatility_order_fields == [ "", ""] } + Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { order.serialize_pegged_order_fields == [] } + Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { order.serialize_peg_best_and_mid == [] } + 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..589fa5a --- /dev/null +++ b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb @@ -0,0 +1,43 @@ +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 'Volatility order on RUT Options' do + + 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: 20, 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 == [ 20, 2] } + end + context "Normal order conditions apply" do + Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + Then { order.serialize_conditions == [ 0 ] } + Then { order.serialize_algo == [ "" ] } + Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } + Then { order.serialize_delta_neutral_order_fields == [ "", ""] } + Then { order.serialize_pegged_order_fields == [] } + Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } + Then { order.serialize_peg_best_and_mid == [] } + end + + end +end From 251f321e028bcb78f0ad329057fe115766f39d81 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sat, 22 Jun 2024 11:38:06 +0200 Subject: [PATCH 42/76] Added Pegged2Stock OrderPrototype --- lib/ib/messages/outgoing/place_order.rb | 6 +-- models/ib/order.rb | 10 ++++ plugins/ib/order_prototypes/pegged.rb | 20 +++----- plugins/ib/symbols/options.rb | 15 ++---- .../order-prototypes/limit_order_spec.rb | 18 ++++--- .../pegged2bench_order_spec.rb | 21 ++++---- .../pegged2stock_order_spec.rb | 51 +++++++++++++++++++ .../order-prototypes/stop_order_spec.rb | 20 ++++---- .../order-prototypes/volatility_order_spec.rb | 14 ++--- 9 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 spec/ib/plugins/order-prototypes/pegged2stock_order_spec.rb diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 220ede9..1b75e58 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -72,11 +72,7 @@ def encode false, # was: order.firm_quote_only || false, desupported in TWS > 981 '', ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here order[:auction_strategy], # AUCTION_MATCH, AUCTION_IMPROVEMENT, AUCTION_TRANSPARENT - order.starting_price , - order.stock_ref_price , - order.delta , - order.stock_range_lower , - order.stock_range_upper , + order.serialize_advanced_option_order_fields, order.override_percentage_constraints, order.serialize_volatility_order_fields, order.serialize_delta_neutral_order_fields diff --git a/models/ib/order.rb b/models/ib/order.rb index 568e243..7328fc6 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -567,6 +567,16 @@ def serialize_pegged_order_fields 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 + def serialize_soft_dollar_tier [ soft_dollar_tier_name, soft_dollar_tier_value diff --git a/plugins/ib/order_prototypes/pegged.rb b/plugins/ib/order_prototypes/pegged.rb index 4636597..c24caad 100644 --- a/plugins/ib/order_prototypes/pegged.rb +++ b/plugins/ib/order_prototypes/pegged.rb @@ -86,24 +86,19 @@ module Pegged2Stock class << self def defaults - super.merge order_type: 'PEG STK' + super.merge order_type: 'PEG STK', tif: :day end - def aliases - Limit.aliases.merge limit_price: :stock_reference_price - end - def requirements - super.merge total_quantity: :decimal, - delta: 'required Delta of the Option', - starting_price: 'initial Limit-Price for the Option' + super.merge total_quantity: :decimal, + delta: 'required Delta of the Option', + starting_price: 'initial Limit-Price for the Option' end def optional - super.merge limit_price: 'Stock Reference Price', - stock_ref_price: '', - stock_range_lower: 'Lowest acceptable Stock Price', - stock_range_upper: 'Highest accepable Stock Price' + 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 @@ -121,7 +116,6 @@ def summary 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. ------------ - Supported Exchanges: (as of Jan 2018): BOX, NASDAQOM, PHLX HERE end end diff --git a/plugins/ib/symbols/options.rb b/plugins/ib/symbols/options.rb index a3c12d3..2926bc8 100644 --- a/plugins/ib/symbols/options.rb +++ b/plugins/ib/symbols/options.rb @@ -80,26 +80,17 @@ def self.contracts exchange: 'SMART', right: :put, expiry: IB::Option.next_expiry , - description: 'IBM-Option Chain ( monthly expiry)'), + description: 'IBM-Option'), :ibm_lazy_expiry => IB::Option.new( symbol: 'IBM', right: :put, strike: 180, exchange: 'SMART', - description: 'IBM-Option Chain with strike 140'), + 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)'), - - :goog100 => IB::Option.new( symbol: 'GOOG', - currency: 'USD', - strike: 100, - multiplier: 100, - right: :call, - exchange: 'SMART', - expiry: IB::Option.next_expiry, - description: 'Google Call Option with monthly expiry') + description: 'IBM-Option Chain ( monthly expiry)') } end end diff --git a/spec/ib/plugins/order-prototypes/limit_order_spec.rb b/spec/ib/plugins/order-prototypes/limit_order_spec.rb index 2075ed4..06bf9af 100644 --- a/spec/ib/plugins/order-prototypes/limit_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -20,17 +20,19 @@ context "Main Order Fields show a Limit order" do Then { order.serialize_main_order_fields == [ "BUY", size, "LMT", price, ""] } end - context "Normal order conditions apply" do + context "Limit orders are submitted as GTC-orders" do Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } - Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + 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_volatility_order_fields == [ "", ""] } - Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } - Then { order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { order.serialize_pegged_order_fields == [] } - Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { order.serialize_peg_best_and_mid == [] } + 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 diff --git a/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb index c544eb6..b1ef95e 100644 --- a/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb @@ -12,7 +12,6 @@ end - STRIKE = 2000 context 'Pegged Bench order on IBM using Microsoft as Benchmark' do @@ -22,9 +21,9 @@ 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 } + 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", "", "" ] } @@ -39,15 +38,15 @@ reference_increment, '' ] } # reference exchange end - context "Normal order conditions apply" do - Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } - Then { order.serialize_volatility_order_fields == [ "", ""] } + 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 == ["", "", "", "", "", ""] } - Then { order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { order.serialize_peg_best_and_mid == [] } + 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 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 index 7006fe5..07c8018 100644 --- a/spec/ib/plugins/order-prototypes/stop_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/stop_order_spec.rb @@ -22,16 +22,16 @@ Then { order.serialize_extended_order_fields == ["GTC", nil, ACCOUNT, "O", 0, nil, true, 0, false, false, nil, 0, false, false] } end - context "Normal order conditions apply" do - Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } - Then { order.serialize_conditions == [ 0 ] } - Then { order.serialize_algo == [ "" ] } - Then { order.serialize_volatility_order_fields == [ "", ""] } - Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } - Then { order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { order.serialize_pegged_order_fields == [] } - Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { order.serialize_peg_best_and_mid == [] } + 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 diff --git a/spec/ib/plugins/order-prototypes/volatility_order_spec.rb b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb index 589fa5a..9b9c275 100644 --- a/spec/ib/plugins/order-prototypes/volatility_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb @@ -28,15 +28,15 @@ context "Volatility specific orderfields are populated; volatility is expessed annualy" do Then { order.serialize_volatility_order_fields == [ 20, 2] } end - context "Normal order conditions apply" do - Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } + 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 == ["", "", "", "", "", ""] } - Then { order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { order.serialize_pegged_order_fields == [] } - Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { order.serialize_peg_best_and_mid == [] } + 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 From 475e89283fad59d20dcd0948ff1070273b3b547b Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sun, 23 Jun 2024 17:56:35 +0200 Subject: [PATCH 43/76] improved methods Order#to_human + Order#as_table --- lib/ib/messages/incoming/open_order.rb | 4 ---- lib/ib/support.rb | 8 ++++---- models/ib/order.rb | 20 +++++++++++++++---- plugins/ib/advanced-account.rb | 11 ++++++++-- plugins/ib/connection-tools.rb | 4 ++-- plugins/ib/order-prototypes.rb | 2 +- .../ib/messages/incoming/account_info_spec.rb | 2 +- .../order-prototypes/limit_order_spec.rb | 8 ++++---- .../pegged2bench_order_spec.rb | 2 +- .../pegged2primary_order_spec.rb | 19 +++++++++--------- .../order-prototypes/stop_order_spec.rb | 8 ++++---- .../order-prototypes/volatility_order_spec.rb | 15 +++++++------- 12 files changed, 58 insertions(+), 45 deletions(-) diff --git a/lib/ib/messages/incoming/open_order.rb b/lib/ib/messages/incoming/open_order.rb index d32d4fd..0eaf328 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -246,10 +246,6 @@ def load [ :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, :bool ], [ :order, :is_O_ms_container, :bool ], [ :order, :discretionary_up_to_limit_price, :bool ], diff --git a/lib/ib/support.rb b/lib/ib/support.rb index 9d0e324..c3b4660 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -2,9 +2,9 @@ # Array : read several formats # Array, String, Symbol, true, false, nil : apply tws.method # -# Apply through: `module aaxx -# using IB::Support a -# ` +# Apply through: module aaxx +# using IB::Support +# module IB module Support @@ -51,7 +51,7 @@ def read_decimal_limit_2 def read_string self.shift rescue "" end - ## reads a string and proofs if NULL == IB::TWS_MAX is present. + ## 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 diff --git a/models/ib/order.rb b/models/ib/order.rb index 7328fc6..89057e9 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -679,21 +679,33 @@ 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 << "id: #{local_id}" if local_id.to_i > 0 "" + misc.join( " " ) + ">" end - def table_header - [ 'account','status' ,'', 'Type', 'tif', 'action', 'amount','price' , 'id/fee' ] + def table_header + [ 'account','status' ,'', 'Type', 'tif', 'action', 'amount','price' , 'misc' ] end def table_row + 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 [ account, order_ref.present? ? order_ref.to_s : status, contract.to_human[1..-2], self[:order_type] , @@ -701,7 +713,7 @@ def table_row action, total_quantity, (limit_price ? "#{limit_price} " : '') + ((aux_price && aux_price != 0) ? "/#{aux_price}" : '') , - commission ? " fee #{commission}" : local_id ] + misc.join( " " ) ] end def serialize_rabbit diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index bac21ee..3fb36ce 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -117,9 +117,14 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ def place_order order:, contract: nil, auto_adjust: true, convert_size: true # adjust the order price to min-tick 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 - qualified_contract = ->(c) { c.is_a?(IB::Contract) && ( c.description.present? || !c.con_id.to_i.zero? || (c.con_id.to_i <0 && c.sec_type == :bag )) } + ( 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 @@ -131,6 +136,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true ## 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] + # declare some variables ib = IB::Connection.current wrong_order = nil the_local_id = nil @@ -140,7 +146,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true ### Default action: raise IB::Transmission Error sa = ib.subscribe( :Alert ) do | msg | # puts "local_id: #{the_local_id}"a - puts msg.inspect + # puts msg.inspect if msg.error_id == the_local_id if [ 110, # The price does not confirm to the minimum price variation for this contract 201, # Order rejected, No Trading permissions @@ -156,6 +162,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true end 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 diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 761925b..8f850be 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -24,7 +24,7 @@ module ConnectionTools # # check_connection reconnects if necessary and returns false if the connection is lost. # - # It delays the process by 6 ms (150 MBit Cable connection) + # It delays the process by 6 ms (150 MBit Cable connection, loc. Europe) # # a = Time.now; G.check_connection; b= Time.now ;b-a # => 0.00066005 @@ -117,7 +117,7 @@ def safe_reconnect class Connection include ConnectionTools - extend Reconnect + extend ReConnect end end diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order-prototypes.rb index c627720..0660c41 100644 --- a/plugins/ib/order-prototypes.rb +++ b/plugins/ib/order-prototypes.rb @@ -112,7 +112,7 @@ def parameters end end -[ :forex, :market, :limit, :stop, :volatility, :premarket, :pegged, :combo ].each do | pt | +[ :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/spec/ib/messages/incoming/account_info_spec.rb b/spec/ib/messages/incoming/account_info_spec.rb index eb1d629..99bb787 100644 --- a/spec/ib/messages/incoming/account_info_spec.rb +++ b/spec/ib/messages/incoming/account_info_spec.rb @@ -67,7 +67,7 @@ it_behaves_like 'Valid AccountValue Object' do let( :the_account_value_object ){ IB::Connection.current.received[:AccountValue].first.account_value } - end + end end # end # describe IB::Messages:Incoming diff --git a/spec/ib/plugins/order-prototypes/limit_order_spec.rb b/spec/ib/plugins/order-prototypes/limit_order_spec.rb index 06bf9af..9a81cb6 100644 --- a/spec/ib/plugins/order-prototypes/limit_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -10,20 +10,20 @@ end - context 'Limit Order to buy 100 Microsoft shares at 200 $ a piece' do + 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 + 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-orders" do + 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 order fields are zero or empty" do + 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 ] } diff --git a/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb index b1ef95e..a9c6858 100644 --- a/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/pegged2bench_order_spec.rb @@ -13,7 +13,7 @@ end - context 'Pegged Bench order on IBM using Microsoft as Benchmark' do + 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 diff --git a/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb b/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb index 6d1ecd5..87096a0 100644 --- a/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/pegged2primary_order_spec.rb @@ -13,7 +13,7 @@ STRIKE = 2000 - context 'Pegged to Primary order on Microsoft with a price cap' do + context 'Pegged to Primary Order Prototype with a price cap' do Given( :stock ){ IB::Stock.new symbol: "MSFT"} @@ -26,16 +26,15 @@ 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 conditions apply" do - Then { order.serialize_auxilery_order_fields == ["", 0, nil, nil, [nil, nil, nil, nil]] } - Then { order.serialize_volatility_order_fields == [ "", ""] } + 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_algo == [ "" ] } - Then { order.serialize_scale_order_fields == ["", "", "", "", "", ""] } - Then { order.serialize_delta_neutral_order_fields == [ "", ""] } - Then { order.serialize_pegged_order_fields == [] } - Then { order.serialize_mifid_order_fields == [[nil, nil], [nil, nil]] } - Then { order.serialize_peg_best_and_mid == [] } + 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 diff --git a/spec/ib/plugins/order-prototypes/stop_order_spec.rb b/spec/ib/plugins/order-prototypes/stop_order_spec.rb index 07c8018..362a65b 100644 --- a/spec/ib/plugins/order-prototypes/stop_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/stop_order_spec.rb @@ -10,19 +10,19 @@ end - context 'Stop order on Microsoft' do + 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 + 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 orders" do + 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 + 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 ] } diff --git a/spec/ib/plugins/order-prototypes/volatility_order_spec.rb b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb index 9b9c275..9eaae22 100644 --- a/spec/ib/plugins/order-prototypes/volatility_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/volatility_order_spec.rb @@ -11,22 +11,21 @@ end - STRIKE = 2000 + context 'Volatility Order Prototype' do - context 'Volatility order on RUT Options' do + Given( :strike ){ 2000 } + Given( :option ){ IB::Symbols::Options.rutw.merge( strike: strike, expiry: IB::Future.next_expiry[0..-3] ).verify.first } - 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: 20, contract: option, account: ACCOUNT } + 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 + 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 + 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 == [ 20, 2] } + 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 ] } From 7ac97cb8c85b9b1824d887a7e7e8060d54a63626 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 24 Jun 2024 06:35:15 +0200 Subject: [PATCH 44/76] Refactoring of IB::Contract --- models/ib/contract.rb | 77 +++++++++---------------------------------- 1 file changed, 16 insertions(+), 61 deletions(-) diff --git a/models/ib/contract.rb b/models/ib/contract.rb index cee1281..ed4e01f 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -113,6 +113,8 @@ 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. # @@ -121,7 +123,7 @@ def default_attributes # :nodoc: # 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 } @@ -201,7 +203,7 @@ def serialize_ib_ruby end # extracts essential attributes of the contract, - # and returns a new contract. + # and returns a new contract. Used for comparism of equality of contracts # # the link to contract-details is __not__ maintained. def essential @@ -209,7 +211,7 @@ 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= self.class.new invariant_attributes.select{|k,_| the_attributes.include? k }.compact new_contract[:description] = if @description.present? @description elsif contract_detail.present? @@ -268,53 +270,16 @@ def merge **new_attributes # 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?) - end - - # All else being equal... - sec_type == other.sec_type + a = ->(e){ e.invariant_attributes.select{|y,_| ![:description, :include_expired].include? y} } + self.call(a) == other.call(a) 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: @@ -369,9 +327,6 @@ def crypto? # :nodoc: end -# def verify # :nodoc: -# error "verify must be overloaded. Please require at least `ib/verify` from the `ib-extenstions` gem " -# end =begin From the release notes of TWS 9.50 @@ -421,7 +376,7 @@ def table_row expiry, { value: multiplier.zero?? "" : multiplier, alignment: :center}, { value: trading_class, alignment: :center}, - {value: right == :none ? "": right, alignment: :center }, + { value: right == :none ? "": right, alignment: :center }, { value: strike.zero? ? "": strike, alignment: :right}, { value: currency, alignment: :center} ] From 99067bcf922546214afb0503ec2a4606e874a493 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 24 Jun 2024 10:34:05 +0200 Subject: [PATCH 45/76] Improved Stock#merge functionality --- models/ib/contract.rb | 9 +++---- models/ib/stock.rb | 10 +++++--- spec/ib/stock_spec.rb | 56 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 spec/ib/stock_spec.rb diff --git a/models/ib/contract.rb b/models/ib/contract.rb index ed4e01f..fc84e2c 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -211,7 +211,8 @@ 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= 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? @@ -227,7 +228,7 @@ def essential # # for convenience # con_id, local_symbol and last_trading_day are resetted, - # the link to contract-details is savaged + # the link to contract-details is savaged # # Example # ge = Stock.new( symbol: :ge).verify.first @@ -270,8 +271,8 @@ def merge **new_attributes # Contract comparison def == other # :nodoc: - a = ->(e){ e.invariant_attributes.select{|y,_| ![:description, :include_expired].include? y} } - self.call(a) == other.call(a) + a = ->(e){ e.essential.invariant_attributes.select{|y,_| ![:description, :include_expired].include? y} } + a.call(self) == a.call(other) end # def to_s diff --git a/models/ib/stock.rb b/models/ib/stock.rb index fc43cd8..8a3f1ae 100644 --- a/models/ib/stock.rb +++ b/models/ib/stock.rb @@ -8,9 +8,13 @@ def default_attributes super.merge :sec_type => :stock, currency:'USD', exchange:'SMART' end - def to_human - att = [ symbol, - currency, ( exchange == 'SMART' ? nil: exchange ), + 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 diff --git a/spec/ib/stock_spec.rb b/spec/ib/stock_spec.rb new file mode 100644 index 0000000..c20cc09 --- /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 symbil 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 + From 606475faaee5a4beb3f12692f6b368bf3bb23b61 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 24 Jun 2024 14:07:56 +0200 Subject: [PATCH 46/76] commissioning of /bin/console --- bin/console | 32 +++++++++++----- lib/ib/connection.rb | 12 ++++++ lib/ib/constants.rb | 3 +- plugins/ib/advanced-account.rb | 11 +++--- plugins/ib/managed-accounts.rb | 11 ------ plugins/ib/process-orders.rb | 68 +++++++++++++++++----------------- 6 files changed, 75 insertions(+), 62 deletions(-) diff --git a/bin/console b/bin/console index c99ba82..669697c 100755 --- a/bin/console +++ b/bin/console @@ -66,30 +66,44 @@ read_yml = -> (key) do C = Connection.new client_id: client_id, host: host, connect: false do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| c.activate_plugin 'connection-tools' c.activate_plugin 'verify' + c.activate_plugin 'process-orders' + c.activate_plugin 'advanced-account' c.activate_plugin 'managed-accounts' - 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( :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] " } +# c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } + c.subscribe(IB::Messages::Incoming::AccountUpdateTime){ } c.initialize_managed_accounts + c.initialize_order_handling + c.get_account_data + c.request_open_orders end - #C.logger.level = Logger::FATAL + puts "Connection established on #{host}, client_id #{client_id} used" + + C.activate_plugin "symbols" + C.activate_plugin "market-price" + C.activate_plugin "order-prototypes" + C.activate_plugin "spread-prototypes" + C.logger.level = Logger::ERROR 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 #{host}, client_id #{client_id} used" + puts + puts "--------------------------------- Active Plugins ---------------------------------------------" + puts C.plugins.delete_if{ |x| x =~ /\// }.sort.join(" - ") 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/lib/ib/connection.rb b/lib/ib/connection.rb index 6cbac67..fe40e26 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -473,5 +473,17 @@ def satisfied? *conditions end end end + private + # 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 end # class Connection end # module IB diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index daf91a8..3a5d260 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -197,8 +197,7 @@ module IB '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 , + '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 diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 3fb36ce..efab82b 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -40,9 +40,10 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ 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 + orders # select all orders of the current account else - orders.find_all{|x| x[search_option.first].to_i == search_option.last.to_i } + key,value = search_option + orders.find_all{|x| x[key].to_i == value.to_i } end if contract.present? @@ -145,8 +146,6 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true ### Handle Error messages ### Default action: raise IB::Transmission Error sa = ib.subscribe( :Alert ) do | msg | - # puts "local_id: #{the_local_id}"a - # puts msg.inspect if msg.error_id == the_local_id if [ 110, # The price does not confirm to the minimum price variation for this contract 201, # Order rejected, No Trading permissions @@ -166,7 +165,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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.update_or_create order, :order_ref + self.orders.save_insert order, :order_ref order.auto_adjust if respond_to?( :auto_adjust ) && auto_adjust # /defined in file order_handling.rb if convert_size order.action = order.total_quantity.to_i < 0 ? :sell : :buy unless order.action == :sell @@ -177,7 +176,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank? # 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 - the_contract = order.contract.con_id.to_i >0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil + the_contract = order.contract.con_id.to_i > 0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil the_local_id = order.place the_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 } diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index fbac761..210a06d 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -228,17 +228,6 @@ def subscribe_account_updates continuously: true end # def - 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 - end class Connection diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb index 8e575db..06ab119 100644 --- a/plugins/ib/process-orders.rb +++ b/plugins/ib/process-orders.rb @@ -19,28 +19,9 @@ module IB =end module ProcessOrders -=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 def initialize_order_handling - tws.subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg| + + subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg| case msg when IB::Messages::Incoming::CommissionReport @@ -52,21 +33,21 @@ def initialize_order_handling # There is no reference to a contract or an account success = update_order_dependent_object( msg.order_state) do |o| - o.order_states.update_or_create msg.order_state, :status + o.order_states.save_insert msg.order_state, :status end - logger.info { "Order State not assigned-- #{msg.order_state.to_human} ----------" } if success.nil? + 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.update_or_create msg.order, :local_id - this_account.contracts.first_or_create msg.contract, :con_id + 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.update_or_create msg.order, :local_id + this_account.orders.save_insert msg.order, :local_id end # update_ib_order msg ## aus support @@ -81,9 +62,9 @@ def initialize_order_handling 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.first_or_create( IB::OrderState.new( perm_id: o.perm_id, - local_id: o.local_id, - status: 'Filled' ), :status ) + 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} @@ -98,7 +79,7 @@ def initialize_order_handling end # branch end # block - logger.error { "Execution-Record not assigned-- #{msg.execution.to_human} ----------" } if success.nil? + logger.warn { "Execution-Record not assigned-- #{msg.execution.to_human} ----------" } if success.nil? end # case msg.code end # do @@ -113,7 +94,7 @@ def initialize_order_handling def request_open_orders q = Queue.new - subscription = tws.subscribe( :OpenOrderEnd ) { q.push(true) } # signal succsess + subscription = subscribe( :OpenOrderEnd ) { q.push(true) } # signal succsess account_data {| account | account.orders = [] } send_message :RequestAllOpenOrders ## the OpenOrderEnd-message usually appears after 0.1 sec. @@ -122,7 +103,7 @@ def request_open_orders q.pop # wait for OpenOrderEnd or finishing of thread - tws.unsubscribe subscription + unsubscribe subscription if q.closed? 5.times do logger.fatal { "Is the API in read-only modus? No Open Order Message received! "} @@ -137,15 +118,34 @@ def request_open_orders 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 - inlcude ProcessOrders + include ProcessOrders end end ## module IB -end # module From 4537aa7dea02e0c41699bcf0661cbb56d25320d4 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 24 Jun 2024 14:23:39 +0200 Subject: [PATCH 47/76] Update to README --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d963e8d..86a8053 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # ib-api Ruby interface to Interactive Brokers' TWS API -Reimplementation of the basic functions of ib-ruby +Reimplementation of ib-ruby --- -__STATUS: Placement of orders is currently broken__ +__STATUS: Gem-Release is still pending --- @@ -22,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 `received-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 @@ -71,10 +71,15 @@ 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 From 2a26d65c6866deab0e05bccff9df2c7159fea3b5 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Wed, 26 Jun 2024 22:04:24 +0200 Subject: [PATCH 48/76] New Order-Prototype: Adaptive, Tests for discretionary orders and Combo's --- lib/support/array_function.rb | 28 ++++++ plugins/ib/advanced-account.rb | 2 +- plugins/ib/order_prototypes/adaptive.rb | 40 ++++++++ plugins/ib/order_prototypes/limit.rb | 22 ++++- plugins/ib/spread-prototypes.rb | 2 +- plugins/ib/symbols.rb | 2 + spec/ib/contracts/butterfly_spec.rb | 50 ++++++++++ spec/ib/contracts/calendar_spec.rb | 47 +++++++++ .../order-prototypes/adaptive_order_spec.rb | 83 ++++++++++++++++ .../discretionary_order_spec.rb | 97 +++++++++++++++++++ spec/main_helper.rb | 7 +- spec/order_helper.rb | 13 +++ spec/support/array_spec.rb | 33 +++++++ 13 files changed, 420 insertions(+), 6 deletions(-) create mode 100644 lib/support/array_function.rb create mode 100644 plugins/ib/order_prototypes/adaptive.rb create mode 100644 spec/ib/contracts/butterfly_spec.rb create mode 100644 spec/ib/contracts/calendar_spec.rb create mode 100644 spec/ib/plugins/order-prototypes/adaptive_order_spec.rb create mode 100644 spec/ib/plugins/order-prototypes/discretionary_order_spec.rb create mode 100644 spec/support/array_spec.rb 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/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index efab82b..2a7cc47 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -209,7 +209,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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 modifiable. + 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 diff --git a/plugins/ib/order_prototypes/adaptive.rb b/plugins/ib/order_prototypes/adaptive.rb new file mode 100644 index 0000000..7934c52 --- /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/limit.rb b/plugins/ib/order_prototypes/limit.rb index a40610c..73846c4 100644 --- a/plugins/ib/order_prototypes/limit.rb +++ b/plugins/ib/order_prototypes/limit.rb @@ -32,7 +32,7 @@ module Discretionary class << self def defaults - Limit.defaults + Limit.defaults end def aliases @@ -44,7 +44,7 @@ def requirements end def optional - super.merge discretionary_amount: :decimal + super.merge discretionary_amount: :decimal end def summary @@ -54,9 +54,25 @@ def summary 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. + 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 diff --git a/plugins/ib/spread-prototypes.rb b/plugins/ib/spread-prototypes.rb index 281148e..dbb4841 100644 --- a/plugins/ib/spread-prototypes.rb +++ b/plugins/ib/spread-prototypes.rb @@ -6,7 +6,7 @@ Strangle build from: Contract. expiry:, p: , c: Vertical.build from: Contract, expiry:, right: , buy: (a strike), sell: (a strike) Calendar.build from: Contract. right:, :strike:, front: (an expiry), back: (an expiry) - Butterfly.buiild from: Contract, right:, strike: , expiry: , front: (long-option strike), back: (long option strike) + Butterfly.build from: Contract, right:, strike: , expiry: , front: (long-option strike), back: (long option strike) StockSpread.fabricate symbol1, symbol2, ratio:[ n, m ] # only for us-stocks diff --git a/plugins/ib/symbols.rb b/plugins/ib/symbols.rb index 32098d6..92a8f70 100644 --- a/plugins/ib/symbols.rb +++ b/plugins/ib/symbols.rb @@ -108,6 +108,8 @@ def [] symbol 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 diff --git a/spec/ib/contracts/butterfly_spec.rb b/spec/ib/contracts/butterfly_spec.rb new file mode 100644 index 0000000..2f22b39 --- /dev/null +++ b/spec/ib/contracts/butterfly_spec.rb @@ -0,0 +1,50 @@ +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 } + 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..6633a9f --- /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: '-1m' + ) } + + 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/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb new file mode 100644 index 0000000..948358a --- /dev/null +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -0,0 +1,83 @@ +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 '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 "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_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 + +# +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# if subject.server_version < 177 +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields +# else +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields +## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields +# end +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +# +# 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..5c1816c --- /dev/null +++ b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb @@ -0,0 +1,97 @@ +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' + + end + + context 'Discretionary Order Prototype' do + + Given( :volatile_stock ){ IB::Stock.new symbol: 'TSLA' } + Given( :size ){ 100 } + Given( :price){ 180 } # 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 + Given( :account ){ IB::Connection.current.clients.detect{| i | i.account == ACCOUNT } } + When( :order_id ){ account.place order: order, contract: volatile_stock } + Then{ account.orders.size > 0 } + Then{ account.orders.last.contract == volatile_stock } + + + end + + end + +# +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# if subject.server_version < 177 +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields +# else +# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields +## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields +# end +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +# +# end +# +# +end # describe IB::Messages:Outgoing diff --git a/spec/main_helper.rb b/spec/main_helper.rb index e6bda1a..0bd7535 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -37,12 +37,17 @@ def should_not_log *patterns ## Connection helpers def establish_connection *plugins - if plugins.include? "managed-accounts" + if plugins.map( &:to_s ).include?("managed-accounts") || plugins.include?("process-orders") || plugins.include?('gateway') OPTS[:connection].merge connect: false ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) do |c| + c.activate_plugin 'verify' + c.activate_plugin 'process-orders' + c.activate_plugin 'advanced-account' c.activate_plugin 'managed-accounts' c.initialize_managed_accounts + c.initialize_order_handling c.get_account_data + c.request_open_orders end else ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) diff --git a/spec/order_helper.rb b/spec/order_helper.rb index 305f427..eb7737d 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -89,6 +89,19 @@ def remove_open_orders end +RSpec.shared_examples_for "serialize limit order fields" do + it "Other Order Fields are zero or empty" do + expect( subject.serialize_auxilery_order_fields.flatten.compact).to eq [ "", 0 ] + expect( subject.serialize_volatility_order_fields.uniq).to eq [ "" ] + expect( subject.serialize_conditions).to eq [ 0 ] + expect( subject.serialize_scale_order_fields.uniq).to eq [""] + expect( subject.serialize_delta_neutral_order_fields.uniq).to eq [ "" ] + expect( subject.serialize_pegged_order_fields).to be_empty + expect( subject.serialize_mifid_order_fields.flatten.compact).to be_empty + expect( subject.serialize_peg_best_and_mid).to be_empty + end # it + +end #RSpec.shared_examples_for 'OpenOrder message' do ## let( :subject ){ the_returned_message } 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 + From d8f72d91ec951ec1b28bcc58cf67831ef4d5937f Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 27 Jun 2024 11:28:40 +0200 Subject: [PATCH 49/76] Adaption of Option/Future#roll and Calendar SpreadPrototype to V10 --- models/ib/future.rb | 1 + models/ib/order.rb | 15 ++--- models/ib/spread.rb | 6 +- plugins/ib/roll.rb | 28 +++++++-- plugins/ib/spread_prototypes/calendar.rb | 23 +++++-- plugins/ib/symbols/combo.rb | 9 ++- spec/combo_helper.rb | 12 ++-- spec/ib/contracts/butterfly_spec.rb | 4 +- spec/ib/contracts/spread_spec.rb | 77 ++++++++++++++++++++++++ 9 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 spec/ib/contracts/spread_spec.rb diff --git a/models/ib/future.rb b/models/ib/future.rb index 5ac129d..490d727 100644 --- a/models/ib/future.rb +++ b/models/ib/future.rb @@ -32,6 +32,7 @@ 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 # # diff --git a/models/ib/order.rb b/models/ib/order.rb index 89057e9..1838bf1 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -684,14 +684,14 @@ def to_human 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 "" + (misc.empty? ? "" : " ") + misc.join( " " ) + ">" end @@ -702,11 +702,12 @@ def table_header def table_row 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 - [ account, order_ref.present? ? order_ref.to_s : status, + 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[1..-2], self[:order_type] , self[:tif], diff --git a/models/ib/spread.rb b/models/ib/spread.rb index 3b30f55..3893405 100644 --- a/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -78,10 +78,12 @@ def add_leg contract, **leg_params 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} added #{contract.to_human}" rescue "Spread: #{contract.to_human}" + self.description = "#{description.nil? ? "": description + " / "} #{leg_description}" rescue "Spread: #{contract.to_human}" - self # return value to enable chaining + self # return object to enable chaining end diff --git a/plugins/ib/roll.rb b/plugins/ib/roll.rb index 7a508f8..e28d502 100644 --- a/plugins/ib/roll.rb +++ b/plugins/ib/roll.rb @@ -2,19 +2,37 @@ module IB module RollFuture # helper method to roll an existing future # - # Argument is the expiry of the target-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] =~ /[mwMW]$/ + 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 - target.add_leg self, action: :buy - target.add_leg new_future, action: :sell + target = IB::Spread.new exchange: exchange, symbol: symbol, currency: currency, + description: " fields.delete(:front), :back => fields.delete(:back) } - error "Specifiaction of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) + error "Specification of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) initialize_spread( underlying ) do | the_spread | leg_prototype = IB::Option.new underlying.attributes .slice( :currency, :symbol, :exchange) @@ -81,7 +87,12 @@ def defaults 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 diff --git a/plugins/ib/symbols/combo.rb b/plugins/ib/symbols/combo.rb index 2b050bc..54c32fe 100644 --- a/plugins/ib/symbols/combo.rb +++ b/plugins/ib/symbols/combo.rb @@ -12,13 +12,12 @@ def self.contracts expiry: IB::Option.next_expiry, trading_class: 'OESX' ) , stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: 5000, back: '2m' , front: IB::Option.next_expiry, trading_class: 'OESX' ), - stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: 4900 ), - front: 4500, back: 5300, - expiry: IB::Option.next_expiry - ), + stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: 4900, + expiry: IB::Option.next_expiry), + front: 4500, back: 5300), stoxx_vertical: IB::Vertical.build( from: IB::Symbols::Index.stoxx, sell: 4500, buy: 5000, right: :put, expiry: IB::Option.next_expiry, trading_class: 'OESX'), - zn_calendar: IB::Calendar.fabricate( IB::Symbols::Futures.zn, '3m') , + 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 diff --git a/spec/combo_helper.rb b/spec/combo_helper.rb index 8c7284c..9544b0b 100644 --- a/spec/combo_helper.rb +++ b/spec/combo_helper.rb @@ -38,29 +38,29 @@ def atm_option stock RSpec.shared_examples 'a valid Estx Combo' do - its( :exchange ) { should eq 'DTB' } + its( :exchange ) { should eq 'EUREX' } its( :symbol ) { should eq "ESTX50" } - its( :market_price ) { should be_a Numeric } +# 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( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid ZN-FUT Combo' do - its( :exchange ) { should eq 'ECBOT' } + its( :exchange ) { should eq 'CBOT' } its( :symbol ) { should eq "ZN" } - its( :market_price ) { should be_a Numeric } +# 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( :market_price ) { should be_a Numeric } end RSpec.shared_examples 'a valid Spread' do diff --git a/spec/ib/contracts/butterfly_spec.rb b/spec/ib/contracts/butterfly_spec.rb index 2f22b39..093fa80 100644 --- a/spec/ib/contracts/butterfly_spec.rb +++ b/spec/ib/contracts/butterfly_spec.rb @@ -18,8 +18,8 @@ close_connection end - let ( :the_option ){ IB::Symbols::Options.stoxx.merge( strike: 5000 ) } - let ( :the_bag ){ IB::Symbols::Combo::stoxx_butterfly } + 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 )} diff --git a/spec/ib/contracts/spread_spec.rb b/spec/ib/contracts/spread_spec.rb new file mode 100644 index 0000000..8a253f8 --- /dev/null +++ b/spec/ib/contracts/spread_spec.rb @@ -0,0 +1,77 @@ +require 'combo_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.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" do +# subject { the_spread.serialize_rabbit } +# +# its(:keys){ should eq ["Spread", "legs", "combo_legs", 'misc'] } +# +# it "serializes the contract" do +# expect( IB::Spread.build_from_json( subject)).to eq the_spread +# end +# +# +# it "json acts as valid transport medium" do +# json_medium = subject.to_json +# expect( IB::Spread.build_from_json( JSON.parse( json_medium ))).to eq the_spread +# end +# +# 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 From 5e1c0fee30cae9b1aadede468f6c75fb6c631333 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 28 Jun 2024 08:30:48 +0200 Subject: [PATCH 50/76] Refactoring of serialisation of combo-legs in the order-process --- lib/ib/messages/outgoing/place_order.rb | 42 +++------------- models/ib/bag.rb | 17 +++++++ models/ib/order.rb | 16 +++++- spec/ib/contracts/spread_spec.rb | 50 ++++++++++++------- .../order-prototypes/adaptive_order_spec.rb | 1 + 5 files changed, 71 insertions(+), 55 deletions(-) diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 1b75e58..06ab5eb 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -17,37 +17,7 @@ def encode fields << contract.serialize_short(:primary_exchange, :sec_id_type) fields << order.serialize_main_order_fields fields << order.serialize_extended_order_fields - - # Send combo legs for BAG requests (srv v8 and above) - if contract.bag? - fields.push(combo_legs.size) - fields += combo_legs.map do |the_leg| - array = [ - the_leg.con_id, - the_leg.ratio, - the_leg.side.to_sup, - the_leg.exchange, - the_leg[:open_close], - the_leg[:short_sale_slot], - the_leg.designated_location, - ] - array.push(the_leg.exempt_code) if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] # 51 - array - end.flatten - - # TODO: order_combo_leg? - if server_version >= KNOWN_SERVERS[:min_server_ver_order_combo_legs_price] # 61 - fields.push(contract.combo_legs.size) - fields += contract.combo_legs.map { |leg| leg.price || '' } - end - - # TODO: smartComboRoutingParams - if server_version >= KNOWN_SERVERS[:min_server_ver_smart_combo_routing_params] # 57 - fields.push(order.combo_params.size) - fields += order.combo_params.to_a - end - end - + fields << order.serialize_combo_legs fields << order.serialize_auxilery_order_fields # incluing advisory order fields if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] @@ -94,7 +64,7 @@ def encode fields.push order.clearing_account fields.push order.clearing_intent - fields.push(order.not_held) if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] #44 + fields.push(order.not_held) # if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] #44 if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral] # 40 fields += contract.serialize_under_comp @@ -108,14 +78,14 @@ def encode end fields.push(order.what_if) - fields.push(order.serialize_misc_options) if server_version >= KNOWN_SERVERS[:min_server_ver_linking] # 70 - fields.push(order.solicided) if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] # 73 - if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] # 76 + fields.push(order.serialize_misc_options) # if server_version >= KNOWN_SERVERS[:min_server_ver_linking] # 70 + fields.push(order.solicided) #if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] # 73 +# if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] # 76 fields += [ order.random_size, order.random_price ] - end +# end fields << order.serialize_pegged_order_fields # if server_version >= KNOWN_SERVERS[:min_server_ver_pegged_to_benchmark] # 102 diff --git a/models/ib/bag.rb b/models/ib/bag.rb index 807d998..6fc9cf8 100644 --- a/models/ib/bag.rb +++ b/models/ib/bag.rb @@ -44,6 +44,23 @@ def same_legs? other legs_description.split(',').sort == other.legs_description.split(',').sort end + def serialize_legs + [ combo_legs.size, + combo_legs.map do |the_leg| + [ + the_leg.con_id, + the_leg.ratio, + the_leg.side.to_sup, + the_leg.exchange, + the_leg[:open_close], + the_leg[:short_sale_slot], + the_leg.designated_location, + the_leg.exempt_code + ] + end + ] + end + # Contract comparison def == other super && same_legs?(other) diff --git a/models/ib/order.rb b/models/ib/order.rb index 1838bf1..bcdce37 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -292,7 +292,7 @@ class Order < IB::Base # CondPriceMax, 62.0; -- max and min-price # CondPriceMin.;60.0 - prop :etrade_only, :firm_quote_only, :nbbo_price_cap + 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 @@ -380,7 +380,7 @@ def default_attributes # default valus are taken from order.java :auction_strategy => :none, :aux_price => server_version < KNOWN_SERVERS[ :min_server_ver_trailing_percent ] ? 0 : '', :block_order => false, - :combo_params =>[], #{}, + :combo_params => Hash.new, :conditions => [], :continuous_update => 0, :delta => "", @@ -439,6 +439,18 @@ def default_attributes # default valus are taken from order.java ) # closing of merge end + def serialize_combo_legs + if contract.bag? + [ contract.serialize_legs, + leg_prices.size, + leg_prices, + combo_params.size, + 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 } diff --git a/spec/ib/contracts/spread_spec.rb b/spec/ib/contracts/spread_spec.rb index 8a253f8..00763e5 100644 --- a/spec/ib/contracts/spread_spec.rb +++ b/spec/ib/contracts/spread_spec.rb @@ -1,4 +1,4 @@ -require 'combo_helper' +require 'order_helper' RSpec.shared_examples 'a valid NQ-FUT Combo' do @@ -7,6 +7,26 @@ # 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.size ).to eq 5 + expect( subject.serialize_combo_legs.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' } @@ -39,22 +59,18 @@ end end -# context "serialize the spread" do -# subject { the_spread.serialize_rabbit } -# -# its(:keys){ should eq ["Spread", "legs", "combo_legs", 'misc'] } -# -# it "serializes the contract" do -# expect( IB::Spread.build_from_json( subject)).to eq the_spread -# end -# -# -# it "json acts as valid transport medium" do -# json_medium = subject.to_json -# expect( IB::Spread.build_from_json( JSON.parse( json_medium ))).to eq the_spread -# 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 ).to eq [ the_spread.serialize_legs, + 0 ,[], 0 , [] ] } + # leg-prices + combo-params + + + + end context "leg management" do subject { the_spread } diff --git a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb index 948358a..aac9e20 100644 --- a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -35,6 +35,7 @@ Then { order.serialize_pegged_order_fields.empty? } Then { order.serialize_mifid_order_fields.flatten.compact.empty? } Then { order.serialize_peg_best_and_mid.empty? } + Then { order.serialize_combo_legs.empty? } end end From 2735308607efb989a82e9f980cc39584a928e07a Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 8 Jul 2024 21:01:59 +0200 Subject: [PATCH 51/76] Implementation of a simple state machine, fitting of plugins --- api.gemspec | 1 + bin/console | 63 ++++----- bin/simple | 10 +- lib/ib-api.rb | 3 + lib/ib/connection.rb | 124 +++++++++++------- lib/ib/messages/outgoing/place_order.rb | 93 +++++-------- lib/ib/plugins.rb | 35 ++--- models/ib/order.rb | 6 +- plugins/ib/advanced-account.rb | 6 +- plugins/ib/connection-tools.rb | 18 ++- plugins/ib/managed-accounts.rb | 16 ++- .../abstract.rb | 0 .../adaptive.rb | 0 .../all-in-one.rb | 0 .../combo.rb | 0 .../forex.rb | 0 .../limit.rb | 0 .../market.rb | 0 .../pegged.rb | 0 .../premarket.rb | 0 .../stop.rb | 0 .../volatility.rb | 0 plugins/ib/process-orders.rb | 3 + .../butterfly.rb | 0 .../calendar.rb | 0 .../stock-spread.rb | 0 .../straddle.rb | 0 .../strangle.rb | 0 .../vertical.rb | 0 .../order-prototypes/adaptive_order_spec.rb | 16 +-- spec/order_helper.rb | 5 + 31 files changed, 201 insertions(+), 198 deletions(-) rename plugins/ib/{order_prototypes => order-prototypes}/abstract.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/adaptive.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/all-in-one.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/combo.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/forex.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/limit.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/market.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/pegged.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/premarket.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/stop.rb (100%) rename plugins/ib/{order_prototypes => order-prototypes}/volatility.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/butterfly.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/calendar.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/stock-spread.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/straddle.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/strangle.rb (100%) rename plugins/ib/{spread_prototypes => spread-prototypes}/vertical.rb (100%) diff --git a/api.gemspec b/api.gemspec index ab34d7e..480eb65 100644 --- a/api.gemspec +++ b/api.gemspec @@ -43,6 +43,7 @@ Gem::Specification.new do |spec| 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' diff --git a/bin/console b/bin/console index 669697c..2f8f9b4 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 , @@ -33,13 +33,12 @@ end # Array # read items from console.yml -read_yml = -> (key) do + read_yml = -> (key) do YAML::load_file( File.expand_path('../console.yml',__FILE__))[key] end - puts - puts ">> IB-Core Interactive Console <<" + puts ">> IB-API Interactive Console <<" puts '-'* 45 puts puts "Namespace is IB ! " @@ -50,57 +49,39 @@ read_yml = -> (key) do 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] + 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, host: host, connect: false do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| - c.activate_plugin 'connection-tools' - c.activate_plugin 'verify' - c.activate_plugin 'process-orders' - c.activate_plugin 'advanced-account' - c.activate_plugin 'managed-accounts' + C = Connection.new client_id: client_id, host: host + C.logger.level = Logger::WARN - # 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(:Alert){ |m| puts "A: "+ m.message } + C.subscribe(:AccountUpdateTime){ } -# c.subscribe( :OpenOrder){ |msg| "Open Order detected and stored: C.received[:OpenOrders] " } - c.subscribe(IB::Messages::Incoming::AccountUpdateTime){ } - - c.initialize_managed_accounts - c.initialize_order_handling - c.get_account_data - c.request_open_orders - end - puts "Connection established on #{host}, client_id #{client_id} used" - - C.activate_plugin "symbols" - C.activate_plugin "market-price" - C.activate_plugin "order-prototypes" - C.activate_plugin "spread-prototypes" + C.received = true + C.activate_plugin :connection_tools, :symbols, :market_price, + "order-prototypes", "spread-prototypes", + "advanced_account", 'process_orders' + C.logger.level = Logger::INFO + C.get_account_data + C.request_open_orders C.logger.level = Logger::ERROR + puts "Connection established on #{host}" + unless C.received[:OpenOrder].blank? puts "---------------------------------------- OpenOrders -------------------------------------------" puts C.clients.map{ |c| c.orders.map &:to_human }.flatten.join("\n") end - puts - puts "--------------------------------- Active Plugins ---------------------------------------------" - puts C.plugins.delete_if{ |x| x =~ /\// }.sort.join(" - ") + 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 diff --git a/bin/simple b/bin/simple index 3253c19..3c8c670 100755 --- a/bin/simple +++ b/bin/simple @@ -48,8 +48,8 @@ read_yml = -> (key) do include IB require 'irb' client_id = ARGV[1] || read_yml[:client_id] - specified_port = ARGV[0] || 'Gateway' - port = case specified_port + specified_host = ARGV[0] || 'Gateway' + host = case specified_host when Integer specified_port # just use the number when /^[gG]/ @@ -63,8 +63,8 @@ read_yml = -> (key) do ## 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, connect: false do |c| # future use__ , optional_capacities: "+PACEAPI" do |c| - + 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 }} @@ -81,7 +81,7 @@ read_yml = -> (key) do puts "------------------------------- OpenOrders ----------------------------------" puts C.received[:OpenOrder].to_human.join "\n" end - puts "Connection established on Port #{port}, client_id #{client_id} used" + puts "Connection established through #{host}, client_id #{client_id} used" puts puts "----> C points to the connection-instance" puts diff --git a/lib/ib-api.rb b/lib/ib-api.rb index 3c25ca1..b6a28ca 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -6,6 +6,7 @@ require 'class_extensions' require 'logger' require 'terminal-table' +require 'workflow' #require 'ib/version' #require 'ib/connection' @@ -22,6 +23,8 @@ 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", diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index fe40e26..e8f6df6 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -1,4 +1,3 @@ - module IB # Encapsulates API connection to TWS or Gateway class Connection @@ -15,12 +14,9 @@ class Connection 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 @@ -28,17 +24,58 @@ class Connection 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:4002', - port: nil, # IB Gateway connection (default --> demo) 4001: production + 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 + event :disconnect, transitions_to: :disconnected + end + state :disconnected do + event :try_connection, transitions_to: :ready + end + + state :account_based_operations do + event :disconnect, transitions_to: :disconnected + event :initialize_order_handling, transitions_to: :account_based_orderflow + end + + state :account_based_orderflow + + 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 - received: true, # Keep all received messages in a @received Hash - # redis: false, # future plans + # 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 @@ -49,6 +86,7 @@ def initialize host: '127.0.0.1:4002', # 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(':') @@ -64,17 +102,18 @@ def initialize host: '127.0.0.1:4002', @subscribe_lock = Mutex.new @receive_lock = Mutex.new @message_lock = Mutex.new + @connected = false @plugins.each do |name| + puts "activating #{name}" activate_plugin name end - @connected = false @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.logger.progname = "Connection" @next_local_id = msg.local_id self.logger.info { "Got next valid order id: #{@next_local_id}." } end @@ -83,12 +122,10 @@ def initialize host: '127.0.0.1:4002', # Its intended for globally available subscriptions of tws-messages yield self if block_given? - if connect - update_next_order_id - Kernel.exit if @next_local_id.nil? # emergency exit. +# if connect +# Kernel.exit if @next_local_id.nil? # emergency exit. # update_next_order_id should have raised an error - end - Connection.current = self +# end end # read actual order_id and @@ -96,15 +133,8 @@ def initialize host: '127.0.0.1:4002', def update_next_order_id q = Queue.new subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id } - unless connected? - if @plugins.include? "connection-tools" - safe_connect - else - connect() # connect implies requesting NextValidId - end - else - send_message :RequestIds - end + try_connection! unless connected? + send_message :RequestIds th = Thread.new { sleep 5; q.close } @next_local_id = q.pop if q.closed? @@ -117,10 +147,13 @@ def update_next_order_id end ### Working with connection + def connected? + @connected + end # - ### connect can be called directly, but is mostly addressed through update_next_order_id - def connect - logger.progname='IB::Connection#connect' + ### Event – call through Connection-object.try_connection! + def try_connection + logger.progname='IB::Connection#Event:TryConnection' if connected? error "Already connected!" return @@ -135,6 +168,7 @@ def connect @remote_connect_time = DateTime.parse the_message.shift.freeze @local_connect_time = Time.now.freeze + @connected = true end # V100 initial handshake @@ -143,33 +177,26 @@ def connect 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." } - + "#{@remote_connect_time} remote." } start_reader + # 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 - end ### Working with message subscribers @@ -262,8 +289,7 @@ 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 + # 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 @@ -348,8 +374,8 @@ def send_message what, *args end rescue Errno::EPIPE logger.error{ "Broken Pipe, trying to reconnect" } - disconnect - connect + disconnect! + try_connection! retry end ## return the transmitted message @@ -396,7 +422,7 @@ def cancel_order *local_ids def start_reader if @reader_running @reader_thread - elsif connected? + else # connected? # if called frm try_connection, the connected state is not set begin Thread.abort_on_exception = true @reader_running = true @@ -405,8 +431,8 @@ def start_reader logger.fatal e.message Kernel.exit end - else - error "Could not start reader, not connected!", :reader, true +# else +# error "Could not start reader, not connected!", :reader, true end end @@ -473,7 +499,7 @@ def satisfied? *conditions end end end - private +# private # safe access to account-data def account_data account_or_id=nil diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 06ab5eb..693584b 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -13,14 +13,15 @@ def encode error 'contract has to be specified' unless contract.is_a? IB::Contract # send place order msg - fields = [ super ] - fields << contract.serialize_short(:primary_exchange, :sec_id_type) - fields << order.serialize_main_order_fields - fields << order.serialize_extended_order_fields - fields << order.serialize_combo_legs - fields << order.serialize_auxilery_order_fields # incluing advisory order fields - - if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] + fields = [ super , + contract.serialize_short(:primary_exchange, :sec_id_type), + order.serialize_main_order_fields, + order.serialize_extended_order_fields, + order.serialize_combo_legs, + order.serialize_auxilery_order_fields # incluing advisory order fields + ] + + if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] # 103 fields.push(order.model_code ) end @@ -29,7 +30,7 @@ def encode 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.exempt_code) #if server_version >= KNOWN_SERVERS[:min_server_ver_sshortx_old] fields.push(order[:oca_type]) fields += [ @@ -38,10 +39,10 @@ def encode order.all_or_none, 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 - '', ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here - order[:auction_strategy], # AUCTION_MATCH, AUCTION_IMPROVEMENT, AUCTION_TRANSPARENT + 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, @@ -64,43 +65,20 @@ def encode fields.push order.clearing_account fields.push order.clearing_intent - fields.push(order.not_held) # if server_version >= KNOWN_SERVERS[:min_server_ver_not_held] #44 + fields.push(order.not_held) - if server_version >= KNOWN_SERVERS[:min_server_ver_delta_neutral] # 40 - fields += contract.serialize_under_comp - end - - if server_version >= KNOWN_SERVERS[:min_server_ver_algo_orders] # 41 - fields += order.serialize_algo - end - if server_version >= KNOWN_SERVERS[:min_server_ver_algo_id] # 71 - fields.push(order.algo_id) - end + 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) # if server_version >= KNOWN_SERVERS[:min_server_ver_linking] # 70 - fields.push(order.solicided) #if server_version >= KNOWN_SERVERS[:min_server_ver_order_solicited] # 73 -# if server_version >= KNOWN_SERVERS[:min_server_ver_randomize_size_and_price] # 76 - fields += [ - order.random_size, - order.random_price - ] -# end + fields.push(order.serialize_misc_options) + fields.push(order.solicided) + fields << [ order.random_size, order.random_price ] fields << order.serialize_pegged_order_fields -# if server_version >= KNOWN_SERVERS[:min_server_ver_pegged_to_benchmark] # 102 -# if order[:order_type] == 'PEG BENCH' -# fields += [ -# order.reference_contract_id, -# order.is_pegged_change_amount_decrease, -# order.pegged_change_amount, -# order.reference_change_amount, -# order.reference_exchange_id -# ] -# end -# - fields += order.serialize_conditions - fields += [ + fields << order.serialize_conditions + fields << [ order.adjusted_order_type, order.trigger_price, order.limit_price_offset, @@ -109,9 +87,8 @@ def encode order.adjusted_trailing_amount, order.adjustable_trailing_unit ] -# end - fields.push(order.ext_operator) if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] + fields.push(order.ext_operator) if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] # 105 fields << order.serialize_soft_dollar_tier @@ -119,47 +96,47 @@ def encode fields << order.serialize_mifid_order_fields - if server_version >= KNOWN_SERVERS[:min_server_ver_auto_price_for_hedge] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + 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] + if server_version >= KNOWN_SERVERS[:min_server_ver_professional_customer] # 184 fields.push(order.professional_account) end diff --git a/lib/ib/plugins.rb b/lib/ib/plugins.rb index d5293f3..9f7870a 100644 --- a/lib/ib/plugins.rb +++ b/lib/ib/plugins.rb @@ -1,24 +1,27 @@ module IB module Plugins - def activate_plugin name - unless @plugins.include? name - # root= base directory of the ib-api source + def activate_plugin *names root= Pathname.new( File.expand_path("../../../", __FILE__ )) - # plugins are defined in ib-api/plugins/ib - filename= root + "plugins/ib/#{name}.rb" - if filename.exist? - if require filename - @plugins << name - true # return value + + 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 + "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 - error "Could not load Plugin `#{name}` --> #{filename} " + IB::Connection.logger.debug "Already activated plugin #{n}" end - else - error "Plugin `#{name}` not found in `plugins/ib/`" - nil - end - else - IB::Connection.logger.debug "Already activated plugin #{name}" end end end diff --git a/models/ib/order.rb b/models/ib/order.rb index bcdce37..dd3fc22 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -285,9 +285,9 @@ class Order < IB::Base :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: + # 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 diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 2a7cc47..cfdfcd5 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -377,8 +377,10 @@ def complex_position con_id # # # => nil -# # - +# + # + # load managed-accounts first and switch to gateway-mode +Connection.current.activate_plugin 'managed-accounts' class Account include Advanced end diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 8f850be..685290e 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -57,7 +57,6 @@ def check_connection result # return value end - # Alternative to `Connection#connect'. # # Tries to connect to the api. If the connection could not be established, waits # 10 sec. or one minute and reconnects. @@ -65,11 +64,11 @@ def check_connection # Unsuccessful connecting attemps are logged. # # - def safe_connect maximal_count_of_retry=100 + def try_connection maximal_count_of_retry=100 i= -1 begin - connect + _try_connection rescue Errno::ECONNREFUSED => e i+=1 if i < maximal_count_of_retry @@ -93,22 +92,25 @@ def safe_connect maximal_count_of_retry=100 rescue IB::Error => e logger.info e end - true # return success-flag + self # return connection end # def + end module ReConnect def safe_reconnect used_plugins = current.plugins used_client_id = current.client_id + used_host = current.host + used_port = current.port used_received = if current.received.nil? || current.received.empty? false else true end - current.disconnect + current &.disconnect current = nil - c = Connection.new client_id: used_client_id + c = Connection.new client_id: used_client_id, host: used_host, port: used_port end @@ -116,8 +118,10 @@ def safe_reconnect end class Connection + alias _try_connection try_connection include ConnectionTools - extend ReConnect + #extend ReConnect end + end diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index 210a06d..5be47fd 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -72,16 +72,13 @@ def initialize_managed_accounts( force: false ) @accounts = [] if connected? - disconnect + disconnect! sleep(0.1) end - if @plugins.include? "connection-tools" - safe_connect - else - connect() - end + try_connection! result = queue.pop unsubscribe man_id, rec_id, error_id + @accounts end # def @@ -125,6 +122,8 @@ 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? @@ -167,6 +166,8 @@ def get_account_data *accounts, **compatibily_argument 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 @@ -227,10 +228,13 @@ def subscribe_account_updates continuously: true end # subscribe end # def + alias activate_managed_accounts subscribe_account_updates + end class Connection include ManagedAccounts + current.activate_managed_accounts! end end diff --git a/plugins/ib/order_prototypes/abstract.rb b/plugins/ib/order-prototypes/abstract.rb similarity index 100% rename from plugins/ib/order_prototypes/abstract.rb rename to plugins/ib/order-prototypes/abstract.rb diff --git a/plugins/ib/order_prototypes/adaptive.rb b/plugins/ib/order-prototypes/adaptive.rb similarity index 100% rename from plugins/ib/order_prototypes/adaptive.rb rename to plugins/ib/order-prototypes/adaptive.rb diff --git a/plugins/ib/order_prototypes/all-in-one.rb b/plugins/ib/order-prototypes/all-in-one.rb similarity index 100% rename from plugins/ib/order_prototypes/all-in-one.rb rename to plugins/ib/order-prototypes/all-in-one.rb diff --git a/plugins/ib/order_prototypes/combo.rb b/plugins/ib/order-prototypes/combo.rb similarity index 100% rename from plugins/ib/order_prototypes/combo.rb rename to plugins/ib/order-prototypes/combo.rb diff --git a/plugins/ib/order_prototypes/forex.rb b/plugins/ib/order-prototypes/forex.rb similarity index 100% rename from plugins/ib/order_prototypes/forex.rb rename to plugins/ib/order-prototypes/forex.rb diff --git a/plugins/ib/order_prototypes/limit.rb b/plugins/ib/order-prototypes/limit.rb similarity index 100% rename from plugins/ib/order_prototypes/limit.rb rename to plugins/ib/order-prototypes/limit.rb diff --git a/plugins/ib/order_prototypes/market.rb b/plugins/ib/order-prototypes/market.rb similarity index 100% rename from plugins/ib/order_prototypes/market.rb rename to plugins/ib/order-prototypes/market.rb diff --git a/plugins/ib/order_prototypes/pegged.rb b/plugins/ib/order-prototypes/pegged.rb similarity index 100% rename from plugins/ib/order_prototypes/pegged.rb rename to plugins/ib/order-prototypes/pegged.rb diff --git a/plugins/ib/order_prototypes/premarket.rb b/plugins/ib/order-prototypes/premarket.rb similarity index 100% rename from plugins/ib/order_prototypes/premarket.rb rename to plugins/ib/order-prototypes/premarket.rb diff --git a/plugins/ib/order_prototypes/stop.rb b/plugins/ib/order-prototypes/stop.rb similarity index 100% rename from plugins/ib/order_prototypes/stop.rb rename to plugins/ib/order-prototypes/stop.rb diff --git a/plugins/ib/order_prototypes/volatility.rb b/plugins/ib/order-prototypes/volatility.rb similarity index 100% rename from plugins/ib/order_prototypes/volatility.rb rename to plugins/ib/order-prototypes/volatility.rb diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb index 06ab119..02938b7 100644 --- a/plugins/ib/process-orders.rb +++ b/plugins/ib/process-orders.rb @@ -146,6 +146,9 @@ def update_order_dependent_object order_dependent_object # :nodoc: 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/spread_prototypes/butterfly.rb b/plugins/ib/spread-prototypes/butterfly.rb similarity index 100% rename from plugins/ib/spread_prototypes/butterfly.rb rename to plugins/ib/spread-prototypes/butterfly.rb diff --git a/plugins/ib/spread_prototypes/calendar.rb b/plugins/ib/spread-prototypes/calendar.rb similarity index 100% rename from plugins/ib/spread_prototypes/calendar.rb rename to plugins/ib/spread-prototypes/calendar.rb diff --git a/plugins/ib/spread_prototypes/stock-spread.rb b/plugins/ib/spread-prototypes/stock-spread.rb similarity index 100% rename from plugins/ib/spread_prototypes/stock-spread.rb rename to plugins/ib/spread-prototypes/stock-spread.rb diff --git a/plugins/ib/spread_prototypes/straddle.rb b/plugins/ib/spread-prototypes/straddle.rb similarity index 100% rename from plugins/ib/spread_prototypes/straddle.rb rename to plugins/ib/spread-prototypes/straddle.rb diff --git a/plugins/ib/spread_prototypes/strangle.rb b/plugins/ib/spread-prototypes/strangle.rb similarity index 100% rename from plugins/ib/spread_prototypes/strangle.rb rename to plugins/ib/spread-prototypes/strangle.rb diff --git a/plugins/ib/spread_prototypes/vertical.rb b/plugins/ib/spread-prototypes/vertical.rb similarity index 100% rename from plugins/ib/spread_prototypes/vertical.rb rename to plugins/ib/spread-prototypes/vertical.rb diff --git a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb index aac9e20..f07daab 100644 --- a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -1,4 +1,5 @@ require 'main_helper' +require 'order_helper' RSpec.describe IB::Order do @@ -23,19 +24,12 @@ 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 + context "Algo specific fields are serialized" do Then { order.serialize_algo == [ "Adaptive", 1, ["adaptivePriority", "Normal"] ] } 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_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? } - Then { order.serialize_combo_legs.empty? } + context "Order specifies as Limit" do + subject{ order } + it_behaves_like "serialize limit order fields" end end diff --git a/spec/order_helper.rb b/spec/order_helper.rb index eb7737d..16ee52d 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -90,6 +90,10 @@ def remove_open_orders end RSpec.shared_examples_for "serialize limit order fields" do + + it "Main Order Fields show a Limit Order" do + expect( subject.serialize_main_order_fields.at 2).to match /LMT/ + end it "Other Order Fields are zero or empty" do expect( subject.serialize_auxilery_order_fields.flatten.compact).to eq [ "", 0 ] expect( subject.serialize_volatility_order_fields.uniq).to eq [ "" ] @@ -99,6 +103,7 @@ def remove_open_orders expect( subject.serialize_pegged_order_fields).to be_empty expect( subject.serialize_mifid_order_fields.flatten.compact).to be_empty expect( subject.serialize_peg_best_and_mid).to be_empty + expect( subject.serialize_combo_legs).to be_empty end # it end From 013bb7c31ffc3bd7d93d85f8a52bdaa23b0b2149 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Wed, 10 Jul 2024 08:09:49 +0200 Subject: [PATCH 52/76] refacturing of equality of contracts (def ==), receiving tick-prices as big-decimal-values; auto-adjust now works as expected --- lib/ib/connection.rb | 5 +- lib/ib/messages/incoming/tick_price.rb | 2 +- models/ib/contract.rb | 12 ++++- plugins/ib/advanced-account.rb | 8 +-- plugins/ib/auto-adjust.rb | 4 +- plugins/ib/connection-tools.rb | 17 ++++++- plugins/ib/market-price.rb | 3 +- spec/ib/connect_spec.rb | 52 ++++++++++++++++++++ spec/ib/connection_spec.rb | 8 +-- spec/ib/orders/account_spec.rb | 68 ++++++++++++++++++++++++++ spec/main_helper.rb | 39 +++++++-------- 11 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 spec/ib/orders/account_spec.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index e8f6df6..82ddc44 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -55,6 +55,7 @@ def workflow_state end state :disconnected do event :try_connection, transitions_to: :ready + event :activate_managed_accounts, transitions_to: :gateway_mode end state :account_based_operations do @@ -62,7 +63,9 @@ def workflow_state event :initialize_order_handling, transitions_to: :account_based_orderflow end - state :account_based_orderflow + 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}" } diff --git a/lib/ib/messages/incoming/tick_price.rb b/lib/ib/messages/incoming/tick_price.rb index bc38158..d510003 100644 --- a/lib/ib/messages/incoming/tick_price.rb +++ b/lib/ib/messages/incoming/tick_price.rb @@ -47,7 +47,7 @@ module Incoming TickPrice = def_message [1, 6], AbstractTick, [:ticker_id, :int], [:tick_type, :int], - [:price, :float], + [:price, :decimal], [:size, :int], [:can_auto_execute, :int] class TickPrice diff --git a/models/ib/contract.rb b/models/ib/contract.rb index fc84e2c..2e9b4ba 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -271,8 +271,16 @@ def merge **new_attributes # Contract comparison def == other # :nodoc: - a = ->(e){ e.essential.invariant_attributes.select{|y,_| ![:description, :include_expired].include? y} } - a.call(self) == a.call(other) +# 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 end # def to_s diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index cfdfcd5..ea09ee9 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -166,11 +166,11 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true # 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 respond_to?( :auto_adjust ) && auto_adjust # /defined in file order_handling.rb + order.auto_adjust if ib.plugins.include?( "auto-adjust" ) && auto_adjust if convert_size - order.action = order.total_quantity.to_i < 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_i < 0 - order.total_quantity = order.total_quantity.to_i.abs + 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 # apply non_guarenteed and other stuff bound to the contract to order. order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank? diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb index e9c4959..5cb4f98 100644 --- a/plugins/ib/auto-adjust.rb +++ b/plugins/ib/auto-adjust.rb @@ -70,8 +70,8 @@ def auto_adjust 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_f, min_tick) unless limit_price.to_f.zero? - self.aux_price= adjust_price.call(aux_price.to_f, min_tick) unless aux_price.to_f.zero? + self.limit_price= adjust_price.call(limit_price.to_d, min_tick) unless limit_price.to_d.zero? + self.aux_price= adjust_price.call(aux_price.to_d, min_tick) unless aux_price.to_d.zero? end end end diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 685290e..60ca810 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -45,10 +45,10 @@ def check_connection count +=1 retry rescue IB::Error # not connected - disconnect + disconnect! logger.info{"not connected ... trying to reconnect "} sleep 0.1 - connect + try_connection! count = 0 retry end @@ -95,6 +95,19 @@ def try_connection maximal_count_of_retry=100 self # return connection end # def + private + 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 module ReConnect diff --git a/plugins/ib/market-price.rb b/plugins/ib/market-price.rb index 61893de..a39c561 100644 --- a/plugins/ib/market-price.rb +++ b/plugins/ib/market-price.rb @@ -49,11 +49,10 @@ def market_price delayed: true, thread: false, no_error: false # 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 out own timeout-criteria + # 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. - # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id } # subscribe to TickPrices sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg| diff --git a/spec/ib/connect_spec.rb b/spec/ib/connect_spec.rb index 5f2a482..fdcd8de 100644 --- a/spec/ib/connect_spec.rb +++ b/spec/ib/connect_spec.rb @@ -7,6 +7,58 @@ context "A new connection" do it{ expect( IB::Connection.current ).to be_a IB::Connection } + + it "has the proper state" do + expect( IB::Connection.current.ready? ).to be_truthy + expect( IB::Connection.current.workflow_state ).to eq 'ready' + 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 + ib = IB::Connection.current + expect( ib.ready? ).to be_truthy + expect { ib.disconnect! }.to change { ib.workflow_state }.to 'disconnected' + expect( ib.disconnected? ).to be_truthy + expect( ib.ready? ).to be_falsy + end + 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 "if disconnected, account-based operations can be loaded" do + ib = IB::Connection.current + expect( ib.workflow_state ).to eq 'ready' + expect { ib.activate_plugin :managed_accounts } .to raise_error Workflow::NoTransitionAllowed + expect( ib.ready? ).to be_truthy + expect( ib.plugins ).not_to include "managed-accounts" + expect { ib.disconnect! }.to change{ ib.workflow_state }.to 'disconnected' + expect { ib.activate_plugin :managed_accounts } .not_to raise_error + expect( ib.plugins ).to include "managed-accounts" + expect { ib.initialize_managed_accounts! }.to change{ ib.workflow_state }.to 'account_based_operations' + expect( ib.clients ).to be_an Array + + end end + end diff --git a/spec/ib/connection_spec.rb b/spec/ib/connection_spec.rb index cf8e4ae..3d9b7bc 100644 --- a/spec/ib/connection_spec.rb +++ b/spec/ib/connection_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe IB::Connection do - Given( :ib ) { IB::Connection.new connect: false } + Given( :ib ) { IB::Connection.new } Then { ib.is_a? IB::Connection } # Check if all Messages are defined @@ -17,16 +17,16 @@ describe "Connection tests" do it "connect to localhost" do - c = IB::Connection.new host: OPTS[:connection][:host], port: OPTS[:connection][:port], connect: false + c = IB::Connection.new host: OPTS[:connection][:host], port: OPTS[:connection][:port] expect( c ).to be_a IB::Connection - c.connect + 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', connect: false expect( c ).to be_a IB::Connection - expect{ c.connect }.to raise_error Errno::ECONNREFUSED + expect{ c.try_connection! }.to raise_error Errno::ECONNREFUSED end end diff --git a/spec/ib/orders/account_spec.rb b/spec/ib/orders/account_spec.rb new file mode 100644 index 0000000..f25c908 --- /dev/null +++ b/spec/ib/orders/account_spec.rb @@ -0,0 +1,68 @@ +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' } # trading hours: 2 - 10 am GMT, min-lot-size: 100 + let( :tui ){IB::Stock.new symbol: :tui1, exchange: :smart, currency: :eur } # trading hours: 2 - 10 am GMT, min-lot-size: 100 + + 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 + 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}" + puts "the_price: #{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/main_helper.rb b/spec/main_helper.rb index 0bd7535..6e0c9f9 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -36,38 +36,38 @@ def should_not_log *patterns ## Connection helpers def establish_connection *plugins - + ib = nil + accounts = nil if plugins.map( &:to_s ).include?("managed-accounts") || plugins.include?("process-orders") || plugins.include?('gateway') OPTS[:connection].merge connect: false - ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) do |c| - c.activate_plugin 'verify' - c.activate_plugin 'process-orders' - c.activate_plugin 'advanced-account' - c.activate_plugin 'managed-accounts' - c.initialize_managed_accounts - c.initialize_order_handling - c.get_account_data - c.request_open_orders - end - else + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) + ib.activate_plugin 'verify', 'process-orders', 'advanced-account' + ib.received = true + ib.get_account_data + ib.request_open_orders + accounts = ib.clients.map(&:account) + + else ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) - end - if ib + 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 + else OPTS[:account_verified] = false raise "could not establish connection!" - end + end end @@ -87,9 +87,6 @@ def clean_connection 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 From 1c501d57047d6c7332fe93197e8ead4bd827507b Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 12 Jul 2024 18:32:28 +0200 Subject: [PATCH 53/76] Minor updates to order & co --- lib/ib/connection.rb | 23 ++++++++++++----------- lib/ib/messages/outgoing/place_order.rb | 2 +- models/ib/order.rb | 10 ++++++---- models/ib/order_state.rb | 2 +- plugins/ib/advanced-account.rb | 8 +++++--- plugins/ib/eod.rb | 3 +++ 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 82ddc44..7d8f65a 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -36,35 +36,35 @@ def workflow_state 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 + 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 + event :try_connection, transitions_to: :ready end state :gateway_mode do - event :try_connection, transitions_to: :ready + 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 - event :disconnect, transitions_to: :disconnected + event :disconnect, transitions_to: :disconnected end state :disconnected do - event :try_connection, transitions_to: :ready - event :activate_managed_accounts, transitions_to: :gateway_mode + event :try_connection, transitions_to: :ready + event :activate_managed_accounts, transitions_to: :gateway_mode end state :account_based_operations do - event :disconnect, transitions_to: :disconnected - event :initialize_order_handling, transitions_to: :account_based_orderflow + event :disconnect, transitions_to: :disconnected + event :initialize_order_handling, transitions_to: :account_based_orderflow end state :account_based_orderflow do - event :disconnect, transitions_to: :disconnected + event :disconnect, transitions_to: :disconnected end on_transition do |from, to, triggering_event, *event_args| @@ -402,6 +402,7 @@ def place_order order, contract # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id. def modify_order order, contract + puts "contract: #{contract.to_human}" # order.modify contract, self ## old error "Unable to modify order; local_id not specified" if order.local_id.nil? order.modified_at = Time.now diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 693584b..0fd92c8 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -17,7 +17,7 @@ def encode contract.serialize_short(:primary_exchange, :sec_id_type), order.serialize_main_order_fields, order.serialize_extended_order_fields, - order.serialize_combo_legs, + order.serialize_combo_legs(contract), order.serialize_auxilery_order_fields # incluing advisory order fields ] diff --git a/models/ib/order.rb b/models/ib/order.rb index dd3fc22..591678e 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -338,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 @@ -359,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, @@ -439,7 +441,7 @@ def default_attributes # default valus are taken from order.java ) # closing of merge end - def serialize_combo_legs + def serialize_combo_legs(contract) if contract.bag? [ contract.serialize_legs, leg_prices.size, diff --git a/models/ib/order_state.rb b/models/ib/order_state.rb index ef2bb70..659a3b4 100644 --- a/models/ib/order_state.rb +++ b/models/ib/order_state.rb @@ -145,7 +145,7 @@ def to_human =end def forcast { :init_margin => init_margin_after, - :maint_margin => maint_margin_after_after, + :maint_margin => maint_margin_after, :equity_with_loan => equity_with_loan_after , :commission => commission, :commission_currency=> commission_currency, diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index ea09ee9..02570d6 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -222,7 +222,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true =end - def modify_order local_id: nil, order_ref: nil, order:nil + 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, @@ -253,7 +253,9 @@ def preview order:, contract: nil, **args_which_are_ignored ib = IB::Connection.current the_local_id = nil # put the order into the queue (and exit) if the event is fired - req = ib.subscribe( :OpenOrder ){|m| q << m.order if m.order.local_id.to_i == the_local_id.to_i } + req = ib.subscribe( :OpenOrder ) do |m| + q << m.order if m.order.local_id.to_i == the_local_id.to_i && !m.order.init_margin.nil? + end order.what_if = true order.account = account @@ -313,7 +315,7 @@ def close order:, contract: nil, reverse: false, **args_which_are_ignored # just a wrapper to the Gateway-cancel-order method def cancel order: - Connection.current.cancel_order order + Connection.current.cancel_order order.local_id end ## ToDo ... needs adaption ! diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb index dfed926..a199ef5 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -220,6 +220,9 @@ def eod start: nil, to: nil, duration: nil , what: :trades, polars: false 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" From 99d270a0da50fd4e335813f0259190b16172dde7 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 16 Jul 2024 07:34:28 +0200 Subject: [PATCH 54/76] retab --- conditions/ib/execution_condition.rb | 274 ++++++------- conditions/ib/margin_condition.rb | 218 +++++------ conditions/ib/order_condition.rb | 38 +- conditions/ib/percent_change_condition.rb | 56 +-- conditions/ib/price_condition.rb | 76 ++-- conditions/ib/time_condition.rb | 72 ++-- conditions/ib/volume_condition.rb | 50 +-- lib/ib/base.rb | 2 +- lib/ib/base_properties.rb | 10 +- lib/ib/connection.rb | 302 +++++++-------- lib/ib/constants.rb | 36 +- lib/ib/errors.rb | 4 +- lib/ib/messages.rb | 4 +- lib/ib/messages/abstract_message.rb | 12 +- lib/ib/messages/incoming.rb | 194 +++++----- lib/ib/messages/incoming/abstract_message.rb | 192 +++++----- lib/ib/messages/incoming/abstract_tick.rb | 8 +- lib/ib/messages/incoming/account_message.rb | 14 +- lib/ib/messages/incoming/contract_data.rb | 38 +- lib/ib/messages/incoming/execution_data.rb | 6 +- lib/ib/messages/incoming/histogram_data.rb | 22 +- lib/ib/messages/incoming/historical_data.rb | 12 +- .../incoming/historical_data_update.rb | 2 +- lib/ib/messages/incoming/managed_accounts.rb | 18 +- lib/ib/messages/incoming/next_valid_id.rb | 2 +- lib/ib/messages/incoming/open_order.rb | 18 +- lib/ib/messages/incoming/order_status.rb | 2 +- lib/ib/messages/incoming/portfolio_value.rb | 66 ++-- lib/ib/messages/incoming/position_data.rb | 14 +- lib/ib/messages/incoming/positions_multi.rb | 12 +- lib/ib/messages/incoming/receive_fa.rb | 27 +- lib/ib/messages/incoming/scanner_data.rb | 2 +- lib/ib/messages/incoming/tick_by_tick.rb | 128 +++---- lib/ib/messages/incoming/tick_generic.rb | 1 - lib/ib/messages/incoming/tick_option.rb | 44 +-- lib/ib/messages/incoming/tick_price.rb | 10 +- lib/ib/messages/outgoing.rb | 202 +++++----- lib/ib/messages/outgoing/abstract_message.rb | 28 +- .../messages/outgoing/bar_request_message.rb | 16 +- lib/ib/messages/outgoing/place_order.rb | 2 +- .../outgoing/request_account_summary.rb | 13 +- .../outgoing/request_historical_data.rb | 8 +- .../messages/outgoing/request_market_data.rb | 45 ++- .../outgoing/request_real_time_bars.rb | 6 +- .../outgoing/request_tick_by_tick_data.rb | 10 +- lib/ib/order_condition.rb | 34 +- lib/ib/prepare_data.rb | 2 +- lib/ib/support.rb | 274 ++++++------- lib/support/logging.rb | 54 +-- models/ib/account.rb | 136 +++---- models/ib/account_value.rb | 8 +- models/ib/bag.rb | 6 +- models/ib/combo_leg.rb | 6 +- models/ib/contract.rb | 68 ++-- models/ib/contract_detail.rb | 38 +- models/ib/forex.rb | 2 +- models/ib/option.rb | 34 +- models/ib/option_detail.rb | 56 +-- models/ib/order.rb | 190 ++++----- models/ib/order_state.rb | 24 +- models/ib/portfolio_value.rb | 68 ++-- models/ib/spread.rb | 198 +++++----- models/ib/stock.rb | 34 +- plugins/ib/advanced-account.rb | 206 +++++----- plugins/ib/connection-tools.rb | 52 +-- plugins/ib/eod.rb | 30 +- plugins/ib/greeks.rb | 96 ++--- plugins/ib/managed-accounts.rb | 116 +++--- plugins/ib/market-price.rb | 14 +- plugins/ib/option-chain.rb | 18 +- plugins/ib/order-prototypes.rb | 144 +++---- plugins/ib/order-prototypes/abstract.rb | 98 ++--- plugins/ib/order-prototypes/adaptive.rb | 4 +- plugins/ib/order-prototypes/all-in-one.rb | 42 +- plugins/ib/order-prototypes/combo.rb | 42 +- plugins/ib/order-prototypes/forex.rb | 12 +- plugins/ib/order-prototypes/limit.rb | 112 +++--- plugins/ib/order-prototypes/market.rb | 74 ++-- plugins/ib/order-prototypes/pegged.rb | 116 +++--- plugins/ib/order-prototypes/premarket.rb | 20 +- plugins/ib/order-prototypes/stop.rb | 254 ++++++------ plugins/ib/order-prototypes/volatility.rb | 34 +- plugins/ib/spread-prototypes.rb | 78 ++-- plugins/ib/spread-prototypes/butterfly.rb | 120 +++--- plugins/ib/spread-prototypes/calendar.rb | 68 ++-- plugins/ib/spread-prototypes/stock-spread.rb | 72 ++-- plugins/ib/spread-prototypes/straddle.rb | 56 +-- plugins/ib/spread-prototypes/strangle.rb | 88 ++--- plugins/ib/spread-prototypes/vertical.rb | 92 ++--- plugins/ib/symbols.rb | 112 +++--- plugins/ib/symbols/abstract.rb | 216 +++++------ plugins/ib/symbols/cfd.rb | 2 +- plugins/ib/symbols/combo.rb | 58 +-- plugins/ib/symbols/commodity.rb | 4 +- plugins/ib/symbols/forex.rb | 2 +- plugins/ib/symbols/futures.rb | 42 +- plugins/ib/symbols/index.rb | 50 +-- plugins/ib/symbols/options.rb | 38 +- plugins/ib/symbols/stocks.rb | 56 +-- plugins/ib/verify.rb | 28 +- spec/account_helper.rb | 40 +- spec/combo_helper.rb | 34 +- spec/contract_helper.rb | 50 +-- spec/ib/connect_spec.rb | 8 +- spec/ib/contracts/butterfly_spec.rb | 28 +- spec/ib/contracts/calendar_spec.rb | 38 +- spec/ib/contracts/spread_spec.rb | 52 +-- spec/ib/extensions_spec.rb | 4 +- spec/ib/integration/account_info_spec.rb | 4 +- spec/ib/integration/fundamental_data_spec.rb | 2 +- .../incoming/abstract_message_spec.rb | 360 +++++++++--------- .../ib/messages/incoming/account_info_spec.rb | 62 +-- .../messages/incoming/account_summary_spec.rb | 26 +- .../incoming/account_update_multi_spec.rb | 22 +- spec/ib/messages/incoming/alert_spec.rb | 6 +- .../messages/incoming/contract_data_spec.rb | 50 +-- .../messages/incoming/head_time_stamp_spec.rb | 8 +- .../messages/incoming/histogram_data_spec.rb | 18 +- .../messages/incoming/historical_data_spec.rb | 46 +-- .../incoming/managed_accounts_spec.rb | 12 +- .../messages/incoming/open_position_spec.rb | 50 +-- .../ib/messages/incoming/option_chain_spec.rb | 2 +- .../ib/messages/incoming/order_status_spec.rb | 18 +- .../messages/incoming/position_data_spec.rb | 12 +- .../messages/incoming/positios_multi_spec.rb | 44 +-- spec/ib/messages/incoming/receive_fa_spec.rb | 10 +- .../recieve_multi_account_update_spec.rb | 10 +- spec/ib/orders/account_spec.rb | 62 +-- spec/ib/plugins/auto_adjust_spec.rb | 8 +- spec/ib/plugins/managed_account_spec.rb | 2 +- .../order-prototypes/adaptive_order_spec.rb | 22 +- .../discretionary_order_spec.rb | 22 +- .../order-prototypes/limit_order_spec.rb | 22 +- spec/ib/plugins/verify_spec.rb | 8 +- spec/ib/stock_spec.rb | 2 +- spec/main_helper.rb | 46 +-- spec/order_helper.rb | 208 +++++----- spec/spec.yml | 7 +- spec/spec_helper.rb | 32 +- 139 files changed, 3831 insertions(+), 3840 deletions(-) diff --git a/conditions/ib/execution_condition.rb b/conditions/ib/execution_condition.rb index e3bc542..8672cb4 100644 --- a/conditions/ib/execution_condition.rb +++ b/conditions/ib/execution_condition.rb @@ -2,142 +2,142 @@ 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 - - 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 - - - class VolumeCondition < OrderCondition - using IB::Support # 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 + class ExecutionCondition < OrderCondition using IB::Support # 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 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 + + 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 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 + + + class VolumeCondition < OrderCondition + using IB::Support # 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 IB::Support # 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 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/conditions/ib/margin_condition.rb b/conditions/ib/margin_condition.rb index 81f793a..e1467ed 100644 --- a/conditions/ib/margin_condition.rb +++ b/conditions/ib/margin_condition.rb @@ -2,114 +2,114 @@ 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 - - - class VolumeCondition < OrderCondition - using IB::Support # 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 + class MarginCondition < OrderCondition using IB::Support # 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 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 + + 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 IB::Support # 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 IB::Support # 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 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/conditions/ib/order_condition.rb b/conditions/ib/order_condition.rb index 190af74..ea832c9 100644 --- a/conditions/ib/order_condition.rb +++ b/conditions/ib/order_condition.rb @@ -3,27 +3,27 @@ class OrderCondition < IB::Base 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 + 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_contract_by_con_id + [ contract.con_id , contract.primary_exchange.presence || contract.exchange ] + end - def serialize - [ condition_type, self[:conjunction_connection] ] - end - end + def serialize + [ condition_type, self[:conjunction_connection] ] + end + end diff --git a/conditions/ib/percent_change_condition.rb b/conditions/ib/percent_change_condition.rb index 7960a23..3cbb874 100644 --- a/conditions/ib/percent_change_condition.rb +++ b/conditions/ib/percent_change_condition.rb @@ -3,35 +3,35 @@ module IB - class PercentChangeCondition < OrderCondition + class PercentChangeCondition < OrderCondition using IB::Support # refine Array-method for decoding of IB-Messages - prop :percent_change + 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 + 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 index a2dfce0..87b7c87 100644 --- a/conditions/ib/price_condition.rb +++ b/conditions/ib/price_condition.rb @@ -1,44 +1,44 @@ module IB - class PriceCondition < OrderCondition - using IB::Support # refine Array-method for decoding of IB-Messages + 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 + 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 index 99c2422..9c87881 100644 --- a/conditions/ib/time_condition.rb +++ b/conditions/ib/time_condition.rb @@ -1,42 +1,42 @@ module IB - class TimeCondition < OrderCondition - using IB::Support # refine Array-method for decoding of IB-Messages + 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 + 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 index 6b750ad..6e04257 100644 --- a/conditions/ib/volume_condition.rb +++ b/conditions/ib/volume_condition.rb @@ -3,38 +3,38 @@ module IB - class VolumeCondition < OrderCondition - using IB::Support # refine Array-method for decoding of IB-Messages + class VolumeCondition < OrderCondition + using IB::Support # refine Array-method for decoding of IB-Messages include BaseProperties - prop :volume + prop :volume - def condition_type - 6 - end + 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 + 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 + the_contract = IB::Contract.new con_id: buffer.read_int, exchange: buffer.read_string + m.contract = the_contract + m + end - def serialize + def serialize - super << self[:operator] << volume << serialize_contract_by.con_id - end + 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 + # 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/ib/base.rb b/lib/ib/base.rb index e17cdfb..2f9840a 100644 --- a/lib/ib/base.rb +++ b/lib/ib/base.rb @@ -66,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 5d9ba0a..3e354bc 100644 --- a/lib/ib/base_properties.rb +++ b/lib/ib/base_properties.rb @@ -31,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, @@ -49,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 diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 7d8f65a..edced6a 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -6,11 +6,11 @@ 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 Plugins @@ -75,13 +75,13 @@ def workflow_state def initialize host: '127.0.0.1:4002', # combination of host + port port: nil, - #:port => '7497', # TWS connection --> demo 7496: production + #: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 @@ -92,7 +92,7 @@ def initialize host: '127.0.0.1:4002', # combination of host + port 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(':') + 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 @@ -131,9 +131,9 @@ def initialize host: '127.0.0.1:4002', # combination of host + port # end 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 } try_connection! unless connected? @@ -149,44 +149,44 @@ def update_next_order_id @next_local_id # return next_id end - ### Working with connection + ### Working with connection def connected? @connected end # ### Event – call through Connection-object.try_connection! - def try_connection - logger.progname='IB::Connection#Event:TryConnection' - if connected? - error "Already connected!" - return - end - - self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible - socket.initialising_handshake - socket.decode_message( socket.receive_messages ) do | the_message | - #puts "TheMessage :: #{the_message.inspect}" + def try_connection + logger.progname='IB::Connection#Event:TryConnection' + if connected? + error "Already connected!" + return + end + + self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible + socket.initialising_handshake + 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 + 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 - 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}, " + + 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 + "#{@local_connect_time} local, " + + "#{@remote_connect_time} remote." } + start_reader # update_next_order_id - end + end ### Event – call through Connection-object.disconnect! @@ -244,15 +244,15 @@ 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 + 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 @@ -269,16 +269,16 @@ def clear_received *message_types # 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 @@ -292,8 +292,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 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 @@ -322,13 +322,13 @@ 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 - # 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 + # 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] == "" @@ -339,15 +339,15 @@ def process_messages poll_time = 50 # in msec # 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 # 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 @@ -357,7 +357,7 @@ 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" def send_message what, *args message = case @@ -371,18 +371,18 @@ 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! - try_connection! - retry - end - ## return the transmitted message - message.data[:request_id].presence || true + rescue Errno::EPIPE + logger.error{ "Broken Pipe, trying to reconnect" } + disconnect! + try_connection! + retry + end + ## return the transmitted message + message.data[:request_id].presence || true end alias dispatch send_message # Legacy alias @@ -392,19 +392,19 @@ def send_message what, *args 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 "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 @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 puts "contract: #{contract.to_human}" # 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 send_message :PlaceOrder, :order => order, @@ -440,80 +440,80 @@ def start_reader 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' - - socket.decode_message( socket.receive_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 + 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' + + socket.decode_message( socket.receive_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 # private # safe access to account-data - def account_data account_or_id=nil + 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 + 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 + end end # class Connection end # module IB diff --git a/lib/ib/constants.rb b/lib/ib/constants.rb index 3a5d260..68a767e 100644 --- a/lib/ib/constants.rb +++ b/lib/ib/constants.rb @@ -105,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, @@ -136,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. @@ -162,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 @@ -192,7 +192,7 @@ 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 @@ -212,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, @@ -228,12 +228,12 @@ module IB 'IOPT' => :dutch_option, 'STK' => :stock, 'WAR' => :warrant, - 'ICU' => :icu, - 'ICS' => :ics, - 'BILL' => :bill, - 'BSK' => :basket, - 'FWD' => :forward, - 'FIXED' => :fixed , + '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." @@ -326,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, @@ -349,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/errors.rb b/lib/ib/errors.rb index 396c3a6..bf1e40c 100644 --- a/lib/ib/errors.rb +++ b/lib/ib/errors.rb @@ -18,8 +18,8 @@ class FlexError < RuntimeError class TransmissionError < RuntimeError end - # define a custom ErrorClass which can be fired if a verification fails - class VerifyError < StandardError + # define a custom ErrorClass which can be fired if a verification fails + class VerifyError < StandardError end end # module IB diff --git a/lib/ib/messages.rb b/lib/ib/messages.rb index f4fc854..59e2db0 100644 --- a/lib/ib/messages.rb +++ b/lib/ib/messages.rb @@ -2,8 +2,8 @@ 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 diff --git a/lib/ib/messages/abstract_message.rb b/lib/ib/messages/abstract_message.rb index f710f1f..169f501 100644 --- a/lib/ib/messages/abstract_message.rb +++ b/lib/ib/messages/abstract_message.rb @@ -38,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 @@ -48,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 diff --git a/lib/ib/messages/incoming.rb b/lib/ib/messages/incoming.rb index 4a08c88..e720233 100644 --- a/lib/ib/messages/incoming.rb +++ b/lib/ib/messages/incoming.rb @@ -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"} + 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 + 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,59 +98,59 @@ 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 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 ] + 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 ] + [ :min_tick, :decimal], + [ :exchange, :string ], + [ :snapshot_permissions, :int ] # class TickRequestParameters -# def load -# simple_load -# end +# def load +# simple_load +# end # end RequestManagedAccounts = def_message 17 - AccountSummaryEnd = def_message 64 + AccountSummaryEnd = def_message 64 - PositionDataEnd = def_message 62 + PositionDataEnd = def_message 62 - PositionsMultiEnd = def_message 72 + PositionsMultiEnd = def_message 72 TickSnapshotEnd = def_message 57, [:ticker_id, :int] @@ -158,27 +158,27 @@ def to_human 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] - ) + 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: @@ -242,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 diff --git a/lib/ib/messages/incoming/abstract_message.rb b/lib/ib/messages/incoming/abstract_message.rb index 891cdbc..2febf5b 100644 --- a/lib/ib/messages/incoming/abstract_message.rb +++ b/lib/ib/messages/incoming/abstract_message.rb @@ -2,115 +2,115 @@ #require 'ib/support' require 'ox' module IB - module Messages - module Incoming - using IB::Support # 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 :> \n #{buffer.inspect} \n" - ### 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 index 885ff98..72a7013 100644 --- a/lib/ib/messages/incoming/abstract_tick.rb +++ b/lib/ib/messages/incoming/abstract_tick.rb @@ -16,10 +16,10 @@ def to_human end.compact.join('",') + " >" end - def the_data - @data.reject{|k,_| [:version, :ticker_id].include? k } - end - 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 index ceedf7b..9ade643 100644 --- a/lib/ib/messages/incoming/account_message.rb +++ b/lib/ib/messages/incoming/account_message.rb @@ -7,18 +7,18 @@ module Incoming # Receives previously requested FA configuration information from TWS. - class AccountMessage < AbstractMessage + class AccountMessage < AbstractMessage def account_value @account_value = IB::AccountValue.new @data[:account_value] end - def account_name - @account_name = @data[:account] - end + def account_name + @account_name = @data[:account] + end - def to_human + def to_human " contract_detail) end @@ -57,9 +57,9 @@ def contract_detail @contract_detail = IB::ContractDetail.new @data[:contract_detail] end - def to_human - "" - end + def to_human + "" + end end # ContractData 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 index 05df3cc..34aac06 100644 --- a/lib/ib/messages/incoming/histogram_data.rb +++ b/lib/ib/messages/incoming/histogram_data.rb @@ -2,26 +2,26 @@ module IB module Messages module Incoming - HistogramData = def_message( [89,0], - [:request_id, :int], - [ :number_of_points , :int ]) do - # to human + HistogramData = def_message( [89,0], + [:request_id, :int], + [ :number_of_points , :int ]) do + # to human "" - end + end class HistogramData attr_accessor :results - using IB::Support # extended Array-Class from abstract_message + 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 + { price: buffer.read_decimal, + count: buffer.read_int } + end + end + end diff --git a/lib/ib/messages/incoming/historical_data.rb b/lib/ib/messages/incoming/historical_data.rb index 6cc8385..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,15 +32,15 @@ module Incoming [:count, :int] class HistoricalData attr_accessor :results - using IB::Support # 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, diff --git a/lib/ib/messages/incoming/historical_data_update.rb b/lib/ib/messages/incoming/historical_data_update.rb index 5384b54..c9354e2 100644 --- a/lib/ib/messages/incoming/historical_data_update.rb +++ b/lib/ib/messages/incoming/historical_data_update.rb @@ -10,7 +10,7 @@ module Incoming class HistoricalDataUpdate attr_accessor :results - using IB::Support # extended Array-Class from abstract_message + using IB::Support # extended Array-Class from abstract_message def bar @bar = IB::Bar.new @data[:bar] diff --git a/lib/ib/messages/incoming/managed_accounts.rb b/lib/ib/messages/incoming/managed_accounts.rb index 046a591..1e8b6c8 100644 --- a/lib/ib/messages/incoming/managed_accounts.rb +++ b/lib/ib/messages/incoming/managed_accounts.rb @@ -1,20 +1,20 @@ module IB module Messages - module Incoming + module Incoming ManagedAccounts = def_message 15, [:accounts_list, :string] - class ManagedAccounts - def accounts - accounts_list.split(',').map{|a| Account.new account: a} - end + class ManagedAccounts + def accounts + accounts_list.split(',').map{|a| Account.new account: a} + end - def to_human - "" - end - end + def to_human + "" + end + end end # module Incoming end # module Messages diff --git a/lib/ib/messages/incoming/next_valid_id.rb b/lib/ib/messages/incoming/next_valid_id.rb index 1ee8e59..05e9f12 100644 --- a/lib/ib/messages/incoming/next_valid_id.rb +++ b/lib/ib/messages/incoming/next_valid_id.rb @@ -8,7 +8,7 @@ module Incoming NextValidID = NextValidId = def_message(9, [:local_id, :int]) class NextValidId - using IB::Support + 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 0eaf328..6f20d12 100644 --- a/lib/ib/messages/incoming/open_order.rb +++ b/lib/ib/messages/incoming/open_order.rb @@ -4,12 +4,12 @@ module Incoming using IB::Support # OpenOrder is the longest message with complex processing logics OpenOrder = - def_message [5, 0], # updated to v. 34 according to python (decoder.py processOpenOrder) + 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 ] + # [ con_id, symbol,. sec_type, expiry, strike, right, multiplier, + # exchange, currency, local_symbol, trading_class ] [ :order, :action, :string ], [ :order, :total_quantity, :decimal ], @@ -92,9 +92,9 @@ def status order.status end - def conditions - order.conditions - end + def conditions + order.conditions + end # Object accessors @@ -160,7 +160,7 @@ def load [ :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 +# { tag: buffer.read_string, value: buffer.read_string } # needs testing # end], [ :order, :scale_init_level_size, :int ], @@ -235,7 +235,7 @@ def load #AdjustedOrderParams [ :order, :adjusted_order_type, :string ], [ :order, :trigger_price, :decimal ], - [ :order, :trail_stop_price, :decimal ], # Traillimit orders + [ :order, :trail_stop_price, :decimal ], # Traillimit orders [ :order, :limit_price_offset, :decimal ], [ :order, :adjusted_stop_price, :decimal ], [ :order, :adjusted_stop_limit_price, :decimal ], @@ -269,7 +269,7 @@ def load # 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') 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 4e0d699..1085719 100644 --- a/lib/ib/messages/incoming/portfolio_value.rb +++ b/lib/ib/messages/incoming/portfolio_value.rb @@ -3,39 +3,39 @@ module Messages module Incoming - 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 - - - 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 - -# alias :to_human :portfolio_value - end # PortfolioValue + 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 + + + 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 + +# alias :to_human :portfolio_value + end # PortfolioValue diff --git a/lib/ib/messages/incoming/position_data.rb b/lib/ib/messages/incoming/position_data.rb index 586c185..73d62f2 100644 --- a/lib/ib/messages/incoming/position_data.rb +++ b/lib/ib/messages/incoming/position_data.rb @@ -2,15 +2,15 @@ module IB module Messages module Incoming - PositionData = - def_message( [61,3] , ContractMessage, - [:account, :string], + 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 ] +# [ 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 + [:price, :decimal] + ) do # def to_human " #{contract.to_human} ( Amount #{position}) : Market-Price #{price} >" end diff --git a/lib/ib/messages/incoming/positions_multi.rb b/lib/ib/messages/incoming/positions_multi.rb index 1df7d4a..7a0b4f1 100644 --- a/lib/ib/messages/incoming/positions_multi.rb +++ b/lib/ib/messages/incoming/positions_multi.rb @@ -3,13 +3,13 @@ module Messages module Incoming - PositionsMulti = def_message( 71, ContractMessage, - [ :request_id, :int ], - [ :account, :string ], - [:contract, :contract], # read standard-contract + 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 ]) + [ :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 index 69df5ea..8a26fad 100644 --- a/lib/ib/messages/incoming/receive_fa.rb +++ b/lib/ib/messages/incoming/receive_fa.rb @@ -1,4 +1,3 @@ - module IB module Messages module Incoming @@ -13,19 +12,19 @@ module Incoming # 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 + 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 + 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 08236b8..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 IB::Support # 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 index 95e7189..4e351d7 100644 --- a/lib/ib/messages/incoming/tick_by_tick.rb +++ b/lib/ib/messages/incoming/tick_by_tick.rb @@ -3,74 +3,74 @@ module Messages module Incoming extend Messages # def_message macros - TickByTick = def_message [99, 0], [:ticker_id, :int ], - [ :tick_type, :int], - [ :time, :int_date ] + 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 + ## 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 + 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 + @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 + [: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_generic.rb b/lib/ib/messages/incoming/tick_generic.rb index 3743340..cb84563 100644 --- a/lib/ib/messages/incoming/tick_generic.rb +++ b/lib/ib/messages/incoming/tick_generic.rb @@ -1,4 +1,3 @@ - module IB module Messages module Incoming diff --git a/lib/ib/messages/incoming/tick_option.rb b/lib/ib/messages/incoming/tick_option.rb index fa7e1b2..f21e69c 100644 --- a/lib/ib/messages/incoming/tick_option.rb +++ b/lib/ib/messages/incoming/tick_option.rb @@ -24,37 +24,37 @@ module Incoming [: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 -"- + [: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 "" + "theta: #{"%7.6f" % (theta || -1)}, pv_dividend: #{"%5.3f" % (pv_dividend || -1)}, " + + "underlying @ #{"% 8.3f" % (under_price || -1)} >" end - class TickOption - def greeks - { delta: delta, gamma: gamma, vega: vega, theta: theta } - 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 + def iv + implied_volatility + end + + + def greeks? + greeks.values.any? &:present? + end - end + end end end end diff --git a/lib/ib/messages/incoming/tick_price.rb b/lib/ib/messages/incoming/tick_price.rb index d510003..adf2588 100644 --- a/lib/ib/messages/incoming/tick_price.rb +++ b/lib/ib/messages/incoming/tick_price.rb @@ -50,11 +50,11 @@ module Incoming [:price, :decimal], [:size, :int], [:can_auto_execute, :int] - class TickPrice - def valid? - super && !price.zero? - end - end + class TickPrice + def valid? + super && !price.zero? + end + end end end end diff --git a/lib/ib/messages/outgoing.rb b/lib/ib/messages/outgoing.rb index b91349b..656c4a5 100644 --- a/lib/ib/messages/outgoing.rb +++ b/lib/ib/messages/outgoing.rb @@ -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 @@ -108,42 +108,42 @@ module Outgoing 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 + # 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: + # 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 + "") # 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" @@ -182,7 +182,7 @@ module Outgoing # - 0 = do not override # - 1 = override ExerciseOptions = def_message([ 21, 2 ], - # :request_id, # id -> required # todo : TEST + # :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,41 +267,41 @@ module Outgoing [:contract, :serialize_short], :volatility, :under_price, - [:implied_volatility_options_count, 0], - [:implied_volatility_options_conditions, '']) + [: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 + 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 + # 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' @@ -382,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 @@ -401,7 +401,7 @@ module Outgoing REQ_HISTORICAL_TICKS = 96 REQ_TICK_BY_TICK_DATA = 97 CANCEL_TICK_BY_TICK_DATA = 98 - # ver10 + # ver10 REQ_COMPLETED_ORDERS = 99 REQ_WSH_META_DATA = 100 CANCEL_WSH_META_DATA = 101 diff --git a/lib/ib/messages/outgoing/abstract_message.rb b/lib/ib/messages/outgoing/abstract_message.rb index e4b21cd..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 @@ -14,8 +12,6 @@ def initialize data={} @created_at = Time.now end - - # This causes the message to send itself over the server socket in server[:socket]. # "server" is the @server instance variable from the IB object. # You can also use this to e.g. get the server version number. @@ -46,23 +42,23 @@ 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 redesigned message-types as place-order, historical-data, etc) + # + # 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 ], # 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? diff --git a/lib/ib/messages/outgoing/bar_request_message.rb b/lib/ib/messages/outgoing/bar_request_message.rb index 1506eca..78053a3 100644 --- a/lib/ib/messages/outgoing/bar_request_message.rb +++ b/lib/ib/messages/outgoing/bar_request_message.rb @@ -44,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 @@ -63,14 +63,14 @@ def encode bar_size, data_type.to_s.upcase, @data[:use_rth] , - "" # not suported realtimebars option string - ] + "" # not suported realtimebars option string + ] end end # RequestRealTimeBars - + RequestHistoricalData = def_message [20, 0], BarRequestMessage, - :request_id # autogenerated if not specified + :request_id # autogenerated if not specified # - data = { # :contract => Contract: requested ticker description @@ -186,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 - '' # 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 0fd92c8..236af8a 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -3,7 +3,7 @@ module Messages module Outgoing extend Messages # def_message macros - PlaceOrder = def_message [ 3,0 ] + PlaceOrder = def_message [ 3,0 ] class PlaceOrder def encode diff --git a/lib/ib/messages/outgoing/request_account_summary.rb b/lib/ib/messages/outgoing/request_account_summary.rb index 9574adb..37b59b9 100644 --- a/lib/ib/messages/outgoing/request_account_summary.rb +++ b/lib/ib/messages/outgoing/request_account_summary.rb @@ -1,4 +1,3 @@ - module IB module Messages module Outgoing @@ -60,15 +59,15 @@ module Outgoing currencies. =end - RequestAccountSummary = def_message( 62, - :request_id, # autogenerated if not specified - [:group, 'All'], - :tags ) + RequestAccountSummary = def_message( 62, + :request_id, # autogenerated if not specified + [:group, 'All'], + :tags ) - 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 index dbf8294..7346f71 100644 --- a/lib/ib/messages/outgoing/request_historical_data.rb +++ b/lib/ib/messages/outgoing/request_historical_data.rb @@ -5,7 +5,7 @@ module Outgoing RequestHistoricalData = def_message [20, 0], BarRequestMessage, - :request_id # autogenerated if not specified + :request_id # autogenerated if not specified # - data = { # :contract => Contract: requested ticker description @@ -121,9 +121,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 - '' # 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/request_market_data.rb b/lib/ib/messages/outgoing/request_market_data.rb index c1ddc74..77c4958 100644 --- a/lib/ib/messages/outgoing/request_market_data.rb +++ b/lib/ib/messages/outgoing/request_market_data.rb @@ -1,4 +1,3 @@ - module IB module Messages module Outgoing @@ -11,40 +10,40 @@ module Outgoing [:contract, :serialize_under_comp, []], [:tick_list, ->(tick_list){ tick_list.is_a?(Array) ? tick_list.join(',') : (tick_list || '')}, []], [:snapshot, false], - [:regulatory_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 - # + # ==> 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, id-autogeneration process is performed # :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 @@ -58,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 @@ -88,16 +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. # end end diff --git a/lib/ib/messages/outgoing/request_real_time_bars.rb b/lib/ib/messages/outgoing/request_real_time_bars.rb index 743ab04..831e847 100644 --- a/lib/ib/messages/outgoing/request_real_time_bars.rb +++ b/lib/ib/messages/outgoing/request_real_time_bars.rb @@ -22,7 +22,7 @@ module Outgoing # # 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 @@ -41,8 +41,8 @@ def encode bar_size, data_type.to_s.upcase, @data[:use_rth] , - "" # not suported realtimebars option string - ] + "" # not suported realtimebars option string + ] end end # RequestRealTimeBars diff --git a/lib/ib/messages/outgoing/request_tick_by_tick_data.rb b/lib/ib/messages/outgoing/request_tick_by_tick_data.rb index 6e19b4f..7866bc5 100644 --- a/lib/ib/messages/outgoing/request_tick_by_tick_data.rb +++ b/lib/ib/messages/outgoing/request_tick_by_tick_data.rb @@ -7,12 +7,12 @@ module Outgoing RequestTickByTickData = - def_message [0, 97], :request_id, # autogenerated if not specified + 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 + :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 diff --git a/lib/ib/order_condition.rb b/lib/ib/order_condition.rb index 0f732f5..b5f219d 100644 --- a/lib/ib/order_condition.rb +++ b/lib/ib/order_condition.rb @@ -3,24 +3,24 @@ module IB - class OrderCondition - using IB::Support # refine Array-method for decoding of IB-Messages - # subclasses representing specialized condition types. + 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 + 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 + # 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/prepare_data.rb b/lib/ib/prepare_data.rb index c4c9912..735e583 100644 --- a/lib/ib/prepare_data.rb +++ b/lib/ib/prepare_data.rb @@ -19,7 +19,7 @@ module PrepareData 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 + if block_given? # A user defined decoding-sequence is accepted via block matrize.pack yield else matrize.pack "Na*" diff --git a/lib/ib/support.rb b/lib/ib/support.rb index c3b4660..1f8c04b 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -8,148 +8,148 @@ 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 = BigDecimal(i) 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 - - - 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 + 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 = BigDecimal(i) 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 + + + 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? + # 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? + else + interim = if tags.size.modulo(2).zero? Hash[*tags.flatten] - else - Hash[*tags[0..-2].flatten] # omit the last element - end - # symbolize Hash + 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 - # + end + end + # def read_contract # read a standard contract and return als hash - { con_id: read_int, + { con_id: read_int, symbol: read_string, sec_type: read_string, expiry: read_string, @@ -181,7 +181,7 @@ def read_bar # read a Historical data bar end - alias read_bool read_boolean + alias read_bool read_boolean def tws if blank? diff --git a/lib/support/logging.rb b/lib/support/logging.rb index ce7f434..d5c0fd7 100644 --- a/lib/support/logging.rb +++ b/lib/support/logging.rb @@ -9,37 +9,37 @@ module Support - module Logging - def self.included(base) - base.extend ClassMethods - base.send :define_method, :logger do - base.logger - end - end + 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 + module ClassMethods + def logger + @logger + end - def logger=(logger) - @logger = logger - end + def logger=(logger) + @logger = logger + end - def configure_logger(log=nil) - if log - @logger = log - else + 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.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 # branch + end # def + end # module ClassMethods + end # module Logging end # module Support diff --git a/models/ib/account.rb b/models/ib/account.rb index 075f5a4..67d2284 100644 --- a/models/ib/account.rb +++ b/models/ib/account.rb @@ -1,72 +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 + 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/models/ib/account_value.rb b/models/ib/account_value.rb index f5a63ee..a64ed91 100644 --- a/models/ib/account_value.rb +++ b/models/ib/account_value.rb @@ -6,8 +6,8 @@ class AccountValue < IB::Base 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/models/ib/bag.rb b/models/ib/bag.rb index 6fc9cf8..9f95246 100644 --- a/models/ib/bag.rb +++ b/models/ib/bag.rb @@ -25,9 +25,9 @@ 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 diff --git a/models/ib/combo_leg.rb b/models/ib/combo_leg.rb index 8b941b7..db62a45 100644 --- a/models/ib/combo_leg.rb +++ b/models/ib/combo_leg.rb @@ -20,10 +20,10 @@ class ComboLeg < IB::Base # 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/models/ib/contract.rb b/models/ib/contract.rb index 2e9b4ba..88720a2 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -31,7 +31,7 @@ class Contract < IB::Base :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 @@ -64,9 +64,9 @@ class Contract < IB::Base 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 @@ -132,10 +132,10 @@ def serialize *fields # :nodoc: 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 ), + ## 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], @@ -161,8 +161,8 @@ 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 @@ -202,15 +202,15 @@ def serialize_ib_ruby serialize_long.join(":") end - # 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 + # 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 ] + 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? @@ -221,13 +221,13 @@ def essential "" 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, + # con_id, local_symbol and last_trading_day are resetted, # the link to contract-details is savaged # # Example @@ -266,7 +266,7 @@ def merge **new_attributes ## 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 @@ -305,8 +305,8 @@ def to_human 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 @@ -355,19 +355,19 @@ def crypto? # :nodoc: # 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 ) diff --git a/models/ib/contract_detail.rb b/models/ib/contract_detail.rb index 0718c24..b0e541d 100644 --- a/models/ib/contract_detail.rb +++ b/models/ib/contract_detail.rb @@ -18,11 +18,11 @@ class ContractDetail < IB::Base :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 @@ -98,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/models/ib/forex.rb b/models/ib/forex.rb index 0999d55..1b637ef 100644 --- a/models/ib/forex.rb +++ b/models/ib/forex.rb @@ -3,7 +3,7 @@ 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/option.rb b/models/ib/option.rb index f39327a..0217b7a 100644 --- a/models/ib/option.rb +++ b/models/ib/option.rb @@ -10,9 +10,9 @@ class Option < Contract :message => "should be put or call" - # introduce Option.greek with reference to IB::OptionDetail-dataset - # - has_one :greek , as: :option_detail + # 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 @@ -53,19 +53,19 @@ def default_attributes super.merge :sec_type => :option #self[:description] ||= osi ? osi : "#{symbol} #{strike} #{right} #{expiry}" end - def == other + 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 ) + # 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 + end # returns the verified option for the next (regular) expiry of the contract. @@ -139,9 +139,9 @@ def to_human end # class Option - class FutureOption < Option + class FutureOption < Option def default_attributes super.merge :sec_type => :futures_option - end - end + end + end end # module IB diff --git a/models/ib/option_detail.rb b/models/ib/option_detail.rb index fa97a8d..cc02c2f 100644 --- a/models/ib/option_detail.rb +++ b/models/ib/option_detail.rb @@ -6,26 +6,26 @@ class OptionDetail < IB::Base 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?} @@ -39,30 +39,30 @@ def greeks? end - def prices? - fields = [:implied_volatility, :under_price, :option_price] + 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/models/ib/order.rb b/models/ib/order.rb index 591678e..45aa0c7 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -153,14 +153,14 @@ class Order < IB::Base :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. @@ -183,7 +183,7 @@ class Order < IB::Base # 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 @@ -197,16 +197,16 @@ class Order < IB::Base :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, :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. @@ -220,29 +220,29 @@ class Order < IB::Base :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 + # 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, + :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, + :is_O_ms_container, :advanced_order_reject, :manual_order_time, :min_trade_qty, @@ -285,12 +285,12 @@ class Order < IB::Base :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 + # 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 @@ -298,7 +298,7 @@ class Order < IB::Base 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 @@ -314,7 +314,7 @@ class Order < IB::Base # Order has a collection of OrderStates, 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 @@ -369,28 +369,28 @@ 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 + :algo_id => '' , # order.java # 495 :all_or_none => false, - :auction_strategy => :none, + :auction_strategy => :none, :aux_price => server_version < KNOWN_SERVERS[ :min_server_ver_trailing_percent ] ? 0 : '', :block_order => false, :combo_params => Hash.new, - :conditions => [], + :conditions => [], :continuous_update => 0, :delta => "", :designated_location => '', # order.java # 487 :display_size => nil, :discretionary_amount => 0, :exempt_code => -1, - :ext_operator => '' , # order.java # 499 + :ext_operator => '' , # order.java # 499 :hedge_param => [], :hidden => false, :is_pegged_change_amount_decrease => false, @@ -398,31 +398,31 @@ def default_attributes # default valus are taken from order.java :limit_price => server_version < KNOWN_SERVERS[ :min_server_ver_order_combo_legs_price ] ? 0 : '', :min_quantity => "", :model_code => "", - :not_held => false, # order.java # 494 + :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, :pegged_change_amount => 0.0, - :random_size => false, #oder.java 497 # Vers 76 - :random_price => false, # order.java # 498 # Vers 76 + :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 + :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 =>"", @@ -433,7 +433,7 @@ def default_attributes # default valus are taken from order.java :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 @@ -494,16 +494,16 @@ def serialize_auxilery_order_fields =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.size ] + conditions.map( &:serialize ) + [ conditions_ignore_rth, conditions_cancel_order ] - end - end + def serialize_conditions + if conditions.empty? + [ 0 ] + else + [ conditions.size ] + conditions.map( &:serialize ) + [ conditions_ignore_rth, conditions_cancel_order ] + end + end def serialize_algo return [''] if algo_strategy.blank? @@ -520,12 +520,12 @@ def serialize_advisory_order_fields end def serialize_volatility_order_fields - if volatility.present? + if volatility.present? [ volatility , # Volatility orders self[:volatility_type] ] # default: annual volatility - else - ["",""] - end + else + ["",""] + end end def serialize_delta_neutral_order_fields @@ -536,10 +536,10 @@ def serialize_delta_neutral_order_fields 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 + delta_neutral_open_close, + delta_neutral_short_sale, + delta_neutral_short_sale_slot, + delta_neutral_designated_location ] else ['', ''] @@ -570,12 +570,12 @@ def serialize_scale_order_fields a end 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 ] + 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 @@ -630,17 +630,17 @@ def serialize_peg_best_and_mid end def serialize_misc_options - "" # Vers. 70 + "" # Vers. 70 end # Placement - # - # The Order is only placed, if local_id is not set - # - # Modifies the Order-Object and returns the assigned local_id + # + # 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 + 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? + 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 @@ -650,9 +650,9 @@ def place the_contract=nil, connection=nil # 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 + 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, @@ -731,11 +731,11 @@ def table_row 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/models/ib/order_state.rb b/models/ib/order_state.rb index 659a3b4..e41c875 100644 --- a/models/ib/order_state.rb +++ b/models/ib/order_state.rb @@ -20,7 +20,7 @@ class OrderState < IB::Base :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: @@ -37,7 +37,7 @@ class OrderState < IB::Base :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 @@ -74,8 +74,8 @@ class OrderState < IB::Base 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: @@ -143,13 +143,13 @@ def to_human 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_after, - :maint_margin => maint_margin_after, - :equity_with_loan => equity_with_loan_after , - :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/models/ib/portfolio_value.rb b/models/ib/portfolio_value.rb index 3c2a85e..91fd3dc 100644 --- a/models/ib/portfolio_value.rb +++ b/models/ib/portfolio_value.rb @@ -1,18 +1,18 @@ module IB 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 index 3893405..ad7f841 100644 --- a/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -1,96 +1,96 @@ module IB class Spread < Bag - has_many :legs + has_many :legs - using IB::Support + using IB::Support =begin Parameters: front: YYYMM(DD) - back: {n}w, {n}d or YYYYMM(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) + 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 + # 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) + # 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 + self # return object to enable chaining - end + end - # removes the contract from the spread definition - # - def remove_leg contract_or_position = nil + # 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 @@ -104,45 +104,45 @@ def remove_leg contract_or_position = nil combo_legs.delete_if { |x| x.con_id == the_con_id } self.description = description + " removed #{contract.to_human}" self # make method chainable - end + end # essentail # effectivley clones the object # - def essential + 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 + end - def multiplier - (legs.map(&:multiplier).sum/legs.size).to_i - 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 + # 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= x + super.merge combo_params: [ ['NonGuaranteed', x] ] + end - def non_guaranteed + def non_guaranteed combo_params['NonGuaranteed'] - end + end # optional: specify default order prarmeters for all spreads -# def order_requirements -# super.merge symbol: symbol -# end +# def order_requirements +# super.merge symbol: symbol +# end def as_table @@ -157,20 +157,20 @@ def as_table end - def self.build_from_json container - read_leg = ->(a) do - IB::ComboLeg.new :con_id => a.read_int, + 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 + 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 + object - end - end + end + end end diff --git a/models/ib/stock.rb b/models/ib/stock.rb index 8a3f1ae..297d31d 100644 --- a/models/ib/stock.rb +++ b/models/ib/stock.rb @@ -1,25 +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 + 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 + 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 end diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 02570d6..8f88837 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -35,15 +35,15 @@ def account_data_scan search_key, search_currency=nil (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 + 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 } + orders.find_all{|x| x[key].to_i == value.to_i } end if contract.present? @@ -96,8 +96,8 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ :warning=>"" the_local_id = g.place order: order - => 67 # returns local_id - order.contract # updated contract-record + => 67 # returns local_id + order.contract # updated contract-record => #9534669, :exchange=>"SGX", @@ -222,33 +222,33 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true =end - def modify_order local_id: nil, order_ref: nil, order:nil, contract: nil + 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 + 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 + alias modify modify_order # Preview - # - # Submits a "WhatIf" Order - # - # Returns the order_state.forecast - # - # The order received from the TWS is 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 + # + # Submits a "WhatIf" Order + # + # Returns the order_state.forecast + # + # The order received from the TWS is 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 the_local_id = nil @@ -270,66 +270,66 @@ def preview order:, contract: nil, **args_which_are_ignored 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 - # - # 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 + # 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 + # + # 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? - 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 + 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: + def cancel order: Connection.current.cancel_order order.local_id - end + end ## ToDo ... needs adaption ! - #returns an hash where portfolio_positions are grouped into Watchlists. - # - # Watchlist => [ contract => [ portfoliopositon] , ... ] ] - # + #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 + 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 + 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 + if c.is_a? IB::Bag c.combo_legs.map( &:con_id ).include?( ref_con_id ) else c.con_id == ref_con_id @@ -338,25 +338,25 @@ def organize_portfolio_positions the_watchlistsi #= IB::Gateway.current.active 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]) + 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 # @@ -381,7 +381,7 @@ def complex_position con_id # => nil # # - # load managed-accounts first and switch to gateway-mode + # load managed-accounts first and switch to gateway-mode Connection.current.activate_plugin 'managed-accounts' class Account include Advanced diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 60ca810..85ca762 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -17,45 +17,45 @@ module IB =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. - # + # 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. + # # It delays the process by 6 ms (150 MBit Cable connection, loc. Europe) - # - # a = Time.now; G.check_connection; b= Time.now ;b-a - # => 0.00066005 - # - def check_connection + # + # a = Time.now; G.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 ## + loop do + begin + send_message(:RequestCurrentTime) # 10 ms ## th = Thread.new{ sleep 1 ; q.push nil } result = q.pop count+=1 break if result || count > 10 - rescue IOError, Errno::ECONNREFUSED # connection lost - count +=1 + rescue IOError, Errno::ECONNREFUSED # connection lost + count +=1 retry - rescue IB::Error # not connected - disconnect! + rescue IB::Error # not connected + disconnect! logger.info{"not connected ... trying to reconnect "} sleep 0.1 try_connection! - count = 0 - retry - end - end - unsubscribe z - result # return value - end + count = 0 + retry + end + end + unsubscribe z + result # return value + end # # Tries to connect to the api. If the connection could not be established, waits diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb index a199ef5..3f2905d 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -31,18 +31,18 @@ module IB =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 + 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. @@ -79,8 +79,8 @@ def self.business_days_between(start_date, end_date) end (whole_weeks * 5) + extra_days - end - end + end + end # 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} @@ -308,6 +308,6 @@ def get_bars(end_date_time, duration, bar_size, what_to_show, polars) class Contract include Eod - end # class + end # class end # module IB diff --git a/plugins/ib/greeks.rb b/plugins/ib/greeks.rb index 3e82ec0..c81f7d5 100644 --- a/plugins/ib/greeks.rb +++ b/plugins/ib/greeks.rb @@ -14,63 +14,63 @@ module Greeks # # 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} +# 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 + 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 = [] + 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? + 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 } + #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. + # 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 ) + 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 + 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 + end + end # if sub_id - # initialize »the_id« that is used to identify the received tick messages - # by firing the market data request + # 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 @@ -86,14 +86,14 @@ def request_greeks delayed: true, what: :model, thread: false 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 + tws.unsubscribe sub_id, s_id, e_id, t_id + end # thread + if thread + th # return thread + else + th.join greek - end + end end end class Option diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index 5be47fd..956170f 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -50,19 +50,19 @@ module ManagedAccounts transmission of available managed-accounts. =end - def initialize_managed_accounts( force: false ) + def initialize_managed_accounts( force: false ) queue = Queue.new # 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 " }" } + 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 + end # initialisation of Account after a successful connection - man_id = subscribe( :ManagedAccounts ) do |msg| + man_id = subscribe( :ManagedAccounts ) do |msg| @accounts = msg.accounts send_message( :RequestFA, fa_data_type: 3) end @@ -81,7 +81,7 @@ def initialize_managed_accounts( force: false ) @accounts - end # def + end # def =begin clients returns a list of Account-Objects @@ -120,22 +120,22 @@ def advisor =end def get_account_data *accounts, **compatibily_argument - subscription = subscribe_account_updates( continuously: false ) + subscription = subscribe_account_updates( continuously: false ) download_end = nil # declare variable received_array_status = received self.received = false - accounts = clients if accounts.empty? + 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 + # 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 " } @@ -143,11 +143,11 @@ def get_account_data *accounts, **compatibily_argument 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 = [] + # 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 + 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) @@ -160,10 +160,10 @@ def get_account_data *accounts, **compatibily_argument end # account.organize_portfolio_positions unless IB::Gateway.current.active_watchlists.empty? - else + else logger.info{ "#{account.account} :: Using stored AccountData " } - end - end + end + end send_message :RequestAccountData, subscribe: false ## do this only once unsubscribe subscription @@ -172,19 +172,19 @@ def get_account_data *accounts, **compatibily_argument unsubscribe download_end unless download_end.nil? unsubscribe subscription raise - end + end def all_contracts - clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id) + clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id) end - private + private - # 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 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. # @@ -194,39 +194,39 @@ def all_contracts # IB::Connection.current.unsubscribe subscription # # clears the subscription - # - - def subscribe_account_updates continuously: true - 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 + # + + def subscribe_account_updates continuously: true + 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 - # simply don't cancel the subscription if continuously is specified - # the connected flag is set in any case, indicating that valid data are present + when IB::Messages::Incoming::AccountDownloadEnd + if account.account_values.size > 10 + # simply don't cancel the subscription if continuously is specified + # the connected flag is set in any case, indicating that valid data are present # send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously - account.update_attribute :connected, true ## flag: Account is completely initialized + 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 + 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 } account.portfolio_values << msg.portfolio_value -# msg.portfolio_value.account = account +# msg.portfolio_value.account = account # # link contract -> portfolio value -# account.contracts.find{ |x| x.con_id == msg.contract.con_id } -# .portfolio_values -# .update_or_create( msg.portfolio_value ) { :account } +# account.contracts.find{ |x| x.con_id == msg.contract.con_id } +# .portfolio_values +# .update_or_create( msg.portfolio_value ) { :account } IB::Connection.logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" } end # case - end # account_data - end # subscribe - end # def + end # account_data + end # subscribe + end # def alias activate_managed_accounts subscribe_account_updates diff --git a/plugins/ib/market-price.rb b/plugins/ib/market-price.rb index a39c561..ab2cccd 100644 --- a/plugins/ib/market-price.rb +++ b/plugins/ib/market-price.rb @@ -13,8 +13,8 @@ module MarketPrice # # The result can be customized by a provided block. # -# IB::Symbols::Stocks.sie.market_price{ |x| x } -# -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3} +# 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_-attribute of IB::Contract @@ -30,12 +30,12 @@ module MarketPrice def market_price delayed: true, thread: false, no_error: false - tws= Connection.current # get the initialized ib-ruby instance + 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 ], + 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 @@ -93,7 +93,7 @@ def market_price delayed: true, thread: false, no_error: false 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 + 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} @@ -119,10 +119,10 @@ def market_price delayed: true, thread: false, no_error: false tws.unsubscribe sub_id, s_id, a_id end if thread - th # return thread + th # return thread else th.join - the_price # return + the_price # return end end # end diff --git a/plugins/ib/option-chain.rb b/plugins/ib/option-chain.rb index 7399dca..c49c19f 100644 --- a/plugins/ib/option-chain.rb +++ b/plugins/ib/option-chain.rb @@ -10,7 +10,7 @@ module OptionChain ### 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 (Blank for all available Exchanges) + ### 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 @@ -38,10 +38,10 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', # 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[:exchange] == 'SMART' +# @option_chain_definition = msg.data +# finalize.push(true) +# end if message[:trading_class] == symbol @option_chain_definition = msg.data finalize.push(true) @@ -78,7 +78,7 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', 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.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(',')} " @@ -90,7 +90,7 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', # third Friday of a month monthly_expirations = @option_chain_definition[:expirations].find_all {|y| (15..21).include? y.day } - # puts @option_chain_definition.inspect + # puts @option_chain_definition.inspect option_prototype = -> ( ltd, strike ) do IB::Option.new( symbol: symbol, exchange: @option_chain_definition[:exchange], @@ -142,13 +142,13 @@ def itm_options count: 5, right: :put, ref_price: :request, sort: :strike, exch below_market_price_strikes = chain[-1][-count..-1].reverse end # branch end - end # def + 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}" + # puts "Chain: #{chain}" below_market_price_strikes = chain[-1][-count..-1].reverse else above_market_price_strikes = chain[1][0..count-1] diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order-prototypes.rb index 0660c41..faa4eef 100644 --- a/plugins/ib/order-prototypes.rb +++ b/plugins/ib/order-prototypes.rb @@ -15,35 +15,35 @@ module IB =end - module OrderPrototype + module OrderPrototype #The Module OrderPrototypes provides a wrapper to define even complex ordertypes. # #The Order is build by # -# IB::.order +# IB::.order # #A description is available through # -# puts IB::.summary +# puts IB::.summary # #Nessesary and optional arguments are printed by # -# puts IB::.parameters +# 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) +# > 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) # # # @@ -54,64 +54,64 @@ 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 + 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 diff --git a/plugins/ib/order-prototypes/abstract.rb b/plugins/ib/order-prototypes/abstract.rb index faaaf0b..91f27a9 100644 --- a/plugins/ib/order-prototypes/abstract.rb +++ b/plugins/ib/order-prototypes/abstract.rb @@ -1,67 +1,67 @@ # These modules are used to facilitate referencing of most common Ordertypes module IB - module OrderPrototype + module OrderPrototype - def order **fields + 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 + # 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 + # 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 alternative_parameters + {} + end + def requirements + { action: IB::VALUES[:side], total_quantity: 'also aliased as :size' } + end - def defaults - { tif: :good_till_cancelled } - end + def defaults + { tif: :good_till_cancelled } + end - def optional - { account: 'Account(number) to trade on' } - end + def optional + { account: 'Account(number) to trade on' } + end - def aliases - { total_quantity: :size } - end + def aliases + { total_quantity: :size } + end - def parameters - the_output = ->(var){ var.map{|x| x.join(" --> ") }.join("\n\t: ")} + 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" + "Required : " + the_output[requirements] + "\n --------------- \n" + + "Optional : " + the_output[optional] + "\n --------------- \n" - end + end - end + end end diff --git a/plugins/ib/order-prototypes/adaptive.rb b/plugins/ib/order-prototypes/adaptive.rb index 7934c52..e9574f3 100644 --- a/plugins/ib/order-prototypes/adaptive.rb +++ b/plugins/ib/order-prototypes/adaptive.rb @@ -20,7 +20,7 @@ def requirements def summary - <<-HERE + <<-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 @@ -33,7 +33,7 @@ def summary 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 + HERE end end end diff --git a/plugins/ib/order-prototypes/all-in-one.rb b/plugins/ib/order-prototypes/all-in-one.rb index 1671beb..937997f 100644 --- a/plugins/ib/order-prototypes/all-in-one.rb +++ b/plugins/ib/order-prototypes/all-in-one.rb @@ -7,40 +7,40 @@ module Combo extend OrderPrototype class << self def defaults - ## todo implement serialisation of key/tag Hash to camelCased-keyValue-List + ## 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 + # for the time being, we use the array representation super.merge order_type: :limit , combo_params: [ ['NonGuaranteed', true] ] end def requirements - Limit.requirements + Limit.requirements end def aliases - Limit.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 + <<-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 combo end # module ib diff --git a/plugins/ib/order-prototypes/combo.rb b/plugins/ib/order-prototypes/combo.rb index 1671beb..937997f 100644 --- a/plugins/ib/order-prototypes/combo.rb +++ b/plugins/ib/order-prototypes/combo.rb @@ -7,40 +7,40 @@ module Combo extend OrderPrototype class << self def defaults - ## todo implement serialisation of key/tag Hash to camelCased-keyValue-List + ## 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 + # for the time being, we use the array representation super.merge order_type: :limit , combo_params: [ ['NonGuaranteed', true] ] end def requirements - Limit.requirements + Limit.requirements end def aliases - Limit.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 + <<-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 combo end # module ib diff --git a/plugins/ib/order-prototypes/forex.rb b/plugins/ib/order-prototypes/forex.rb index 0d1056f..702fd27 100644 --- a/plugins/ib/order-prototypes/forex.rb +++ b/plugins/ib/order-prototypes/forex.rb @@ -6,20 +6,20 @@ class << self def defaults - super.merge order_type: :limit , tif: :day + 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' + 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 + <<-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 diff --git a/plugins/ib/order-prototypes/limit.rb b/plugins/ib/order-prototypes/limit.rb index 73846c4..80a48fe 100644 --- a/plugins/ib/order-prototypes/limit.rb +++ b/plugins/ib/order-prototypes/limit.rb @@ -5,25 +5,25 @@ module Limit class << self def defaults - super.merge order_type: :limit + super.merge order_type: :limit end def aliases - super.merge limit_price: :price + super.merge limit_price: :price end def requirements - super.merge limit_price: "also aliased as :price" + 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 + <<-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 @@ -36,27 +36,27 @@ def defaults end def aliases - Limit.aliases.merge discretionary_amount: :dc + Limit.aliases.merge discretionary_amount: :dc end def requirements - Limit.requirements + Limit.requirements end def optional - super.merge discretionary_amount: :decimal + 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 + <<-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 + HERE end def example @@ -81,30 +81,30 @@ module Sweep2Fill class << self def defaults - super.merge order_type: ':limit' , tif: :day, sweep_to_fill: true + super.merge order_type: ':limit' , tif: :day, sweep_to_fill: true end def aliases - Limit.aliases + Limit.aliases end def requirements - Limit.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. - - ------------------------ + <<-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 + HERE end end end @@ -113,25 +113,25 @@ module LimitIfTouched class << self def defaults - Limit.defaults.merge order_type: :limit_if_touched + Limit.defaults.merge order_type: :limit_if_touched end def aliases - Limit.aliases.merge aux_price: :trigger_price + Limit.aliases.merge aux_price: :trigger_price end def requirements - Limit.requirements.merge aux_price: 'also aliased as :trigger_price ' + 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 + <<-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 @@ -142,23 +142,23 @@ module LimitOnClose class << self def defaults - Limit.defaults.merge order_type: :limit_on_close + Limit.defaults.merge order_type: :limit_on_close end def aliases - Limit.aliases + Limit.aliases end def requirements - Limit.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 + <<-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 @@ -168,24 +168,24 @@ module LimitOnOpen class << self def defaults - super.merge order_type: :limit_on_open , tif: :opening_price + super.merge order_type: :limit_on_open , tif: :opening_price end def aliases - Limit.aliases + Limit.aliases end def requirements - Limit.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 + <<-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 diff --git a/plugins/ib/order-prototypes/market.rb b/plugins/ib/order-prototypes/market.rb index 9246a5e..075306a 100644 --- a/plugins/ib/order-prototypes/market.rb +++ b/plugins/ib/order-prototypes/market.rb @@ -6,25 +6,25 @@ module Market class << self def defaults - super.merge order_type: 'MKT' , tif: :day + super.merge order_type: 'MKT' , tif: :day end def aliases - super + super end def requirements - super + 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 + <<-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 @@ -33,30 +33,30 @@ module MarketIfTouched class << self def defaults - super.merge order_type: 'MIT' , tif: :day + super.merge order_type: 'MIT' , tif: :day end def aliases - super + super end def requirements - super + 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 + <<-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 @@ -67,23 +67,23 @@ module MarketOnClose class << self def defaults - super.merge order_type: 'MOC' , tif: :day + super.merge order_type: 'MOC' , tif: :day end def aliases - super + super end def requirements - super + 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 + <<-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 @@ -93,23 +93,23 @@ module MarketOnOpen class << self def defaults - super.merge order_type: 'MOC' , tif: :opening_price + super.merge order_type: 'MOC' , tif: :opening_price end def aliases - super + super end def requirements - super + 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 + <<-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 diff --git a/plugins/ib/order-prototypes/pegged.rb b/plugins/ib/order-prototypes/pegged.rb index c24caad..50390e2 100644 --- a/plugins/ib/order-prototypes/pegged.rb +++ b/plugins/ib/order-prototypes/pegged.rb @@ -4,45 +4,45 @@ module Pegged2Primary class << self def defaults - super.merge order_type: :pegged_to_primary , tif: :day + super.merge order_type: :pegged_to_primary , tif: :day end def aliases - super.merge limit_price: :price_cap, + 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' + super.merge aux_price: 'also aliased as :offset_amount', + limit_price: 'aliased as :price_cap' end def optional - super + 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 + <<-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 @@ -51,32 +51,32 @@ module Pegged2Market class << self def defaults - super.merge order_type: 'PEG MKT' , tif: :day + super.merge order_type: 'PEG MKT' , tif: :day end def aliases - Limit.aliases.merge aux_price: :market_offset + Limit.aliases.merge aux_price: :market_offset end def requirements - super.merge aux_price: :decimal + super.merge aux_price: :decimal end def optional - super + 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 + <<-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 @@ -91,32 +91,32 @@ def defaults def requirements super.merge total_quantity: :decimal, - delta: 'required Delta of the Option', + 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', + 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 + <<-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 diff --git a/plugins/ib/order-prototypes/premarket.rb b/plugins/ib/order-prototypes/premarket.rb index f4afc11..6c147cc 100644 --- a/plugins/ib/order-prototypes/premarket.rb +++ b/plugins/ib/order-prototypes/premarket.rb @@ -5,26 +5,26 @@ module AtAuction class << self def defaults - { order_type: 'MTL' , tif: "AUC"} + { order_type: 'MTL' , tif: "AUC"} end def aliases - super.merge limit_price: :price + super.merge limit_price: :price end def requirements - super.merge limit_price: :decimal + 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 + <<-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 diff --git a/plugins/ib/order-prototypes/stop.rb b/plugins/ib/order-prototypes/stop.rb index 2bf2bb0..a53dfcf 100644 --- a/plugins/ib/order-prototypes/stop.rb +++ b/plugins/ib/order-prototypes/stop.rb @@ -5,30 +5,30 @@ module SimpleStop class << self def defaults - super.merge order_type: :stop + super.merge order_type: :stop end def aliases - super.merge aux_price: :price + super.merge aux_price: :price end def requirements - super.merge aux_price: 'Price where the action is triggert. Aliased as :price' + 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 + <<-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 @@ -37,26 +37,26 @@ module StopLimit class << self def defaults - super.merge order_type: :stop_limit + super.merge order_type: :stop_limit end def aliases - Limit.aliases.merge aux_price: :stop_price + 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' + 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 + <<-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 @@ -65,123 +65,123 @@ module StopProtected class << self def defaults - super.merge order_type: :stop_protected + super.merge order_type: :stop_protected end def aliases - SimpleStop.aliases + SimpleStop.aliases end def requirements - SimpleStop.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 + <<-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 + 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. + 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 + HERE + end + end # def TrailingStopLimit(action:str, quantity:float, lmtPriceOffset:float, # trailingAmount:float, trailStopPrice:float): @@ -198,5 +198,5 @@ def summary # return order # # - end + end end diff --git a/plugins/ib/order-prototypes/volatility.rb b/plugins/ib/order-prototypes/volatility.rb index 0e7d674..a5caa73 100644 --- a/plugins/ib/order-prototypes/volatility.rb +++ b/plugins/ib/order-prototypes/volatility.rb @@ -6,33 +6,33 @@ module Volatility class << self def defaults - { order_type: :volatility, volatility_type: 2 } #default is annual volatility + { order_type: :volatility, volatility_type: 2 } #default is annual volatility end def requirements - super.merge volatility: "the desired Option implied Vola (in %)" + super.merge volatility: "the desired Option implied Vola (in %)" end def aliases - super.merge volatility: :volatility_percent + 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 + <<-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 diff --git a/plugins/ib/spread-prototypes.rb b/plugins/ib/spread-prototypes.rb index dbb4841..417c888 100644 --- a/plugins/ib/spread-prototypes.rb +++ b/plugins/ib/spread-prototypes.rb @@ -15,53 +15,53 @@ module IB # Spreads are created in two ways: # -# (1) IB::Spread::{prototype}.build from: {underlying}, -# trading_class: (optional) -# {other specific attributes} +# (1) IB::Spread::{prototype}.build from: {underlying}, +# trading_class: (optional) +# {other specific attributes} # -# (2) IB::Spread::{prototype}.fabcricate master: [one leg}, -# {other specific attributes} +# (2) IB::Spread::{prototype}.fabcricate master: [one leg}, +# {other specific attributes} # -# They return a freshly instantiated Spread-Object +# They return a freshly instantiated Spread-Object # - module SpreadPrototype + module SpreadPrototype - def build from: , **fields - end + def build from: , **fields + end - def initialize_spread ref_contract = nil, **attributes - error "Initializing of Spread failed – contract is missing" unless ref_contract.is_a?(IB::Contract) + def initialize_spread ref_contract = nil, **attributes + error "Initializing of Spread failed – contract is missing" unless ref_contract.is_a?(IB::Contract) # make sure that :exchange, :symbol and :currency are present - the_contract = ref_contract.merge( **attributes ).verify.first - error "Underlying for Spread is not valid: #{ref_contract.to_human}" if the_contract.nil? - the_spread= IB::Spread.new the_contract.attributes.slice( :exchange, :symbol, :currency ) - error "Initializing of Spread failed – Underling is no Contract" if the_spread.nil? - yield the_spread if block_given? # yield outside mutex controlled verify-environment - the_spread # return_value - end - - def requirements - {} - end - - def defaults - {} - end - - def optional - { } - end - - def parameters - the_output = ->(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 + the_contract = ref_contract.merge( **attributes ).verify.first + error "Underlying for Spread is not valid: #{ref_contract.to_human}" if the_contract.nil? + the_spread= IB::Spread.new the_contract.attributes.slice( :exchange, :symbol, :currency ) + error "Initializing of Spread failed – Underling is no Contract" if the_spread.nil? + yield the_spread if block_given? # yield outside mutex controlled verify-environment + the_spread # return_value + end + + def requirements + {} + end + + def defaults + {} + end + + def optional + { } + end + + def parameters + the_output = ->(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}" diff --git a/plugins/ib/spread-prototypes/butterfly.rb b/plugins/ib/spread-prototypes/butterfly.rb index 3a3d164..efede8a 100644 --- a/plugins/ib/spread-prototypes/butterfly.rb +++ b/plugins/ib/spread-prototypes/butterfly.rb @@ -3,75 +3,75 @@ module IB module Butterfly extend SpreadPrototype - class << self + 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: + # 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 + 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 + 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 + 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 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 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 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" + def requirements + super.merge back: "the strike of the lower bougth option", + front: "the strike of the upper bougth option" - end + end - end # class - end # module + end # class + end # module end # module ib diff --git a/plugins/ib/spread-prototypes/calendar.rb b/plugins/ib/spread-prototypes/calendar.rb index a0b3ac1..57cb40c 100644 --- a/plugins/ib/spread-prototypes/calendar.rb +++ b/plugins/ib/spread-prototypes/calendar.rb @@ -13,9 +13,9 @@ class << self # # Call with # IB::Calendar.fabricate an_option, the_other_expiry - def fabricate master, 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 + 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) @@ -28,9 +28,9 @@ def fabricate master, the_other_expiry # calendar = m.roll expiry: back error "Initialisation of Legs failed" if target.legs.size != 2 - target.description = the_description( target ) + target.description = the_description( target ) target # return fabricated spread - end + end # Build Vertical out of an Underlying @@ -40,31 +40,31 @@ def fabricate master, the_other_expiry # 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:, **fields - underlying = if from.is_a? IB::Option - fields[:right] = from.right unless fields.key?(:right) - fields[:front] = from.expiry unless fields.key(:front) - fields[:strike] = from.strike unless fields.key?(:strike) - 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? +# IB::Calendar.build from: IB::Contract, front: an_expiry, back: an_expiry, +# right: {put or call}, strike: a_strike + def build from:, **fields + underlying = if from.is_a? IB::Option + fields[:right] = from.right unless fields.key?(:right) + fields[:front] = from.expiry unless fields.key(:front) + fields[:strike] = from.strike unless fields.key?(:strike) + 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).verify.first.essential - else - from - end - kind = { :front => fields.delete(:front), :back => fields.delete(:back) } - error "Specification of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) - initialize_spread( underlying ) do | the_spread | + IB::Contract.new( con_id: details.under_con_id, + currency: from.currency).verify.first.essential + else + from + end + kind = { :front => fields.delete(:front), :back => fields.delete(:back) } + error "Specification of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) + initialize_spread( underlying ) do | the_spread | leg_prototype = IB::Option.new underlying.attributes .slice( :currency, :symbol, :exchange) .merge(defaults) .merge( fields ) - kind[:back] = IB::Spread.transform_distance kind[:front], kind[:back] - leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future) + kind[:back] = IB::Spread.transform_distance kind[:front], kind[: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 @@ -74,26 +74,26 @@ def build from:, **fields 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 ) - end - end + 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 + right: :put end - def the_description spread - x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:last_trading_day )].transpose + 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 + 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 index a7c47f3..baf5bdc 100644 --- a/plugins/ib/spread-prototypes/stock-spread.rb +++ b/plugins/ib/spread-prototypes/stock-spread.rb @@ -2,46 +2,46 @@ module IB module StockSpread extend SpreadPrototype - class << self + 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{|y| y.is_a?( IB::Stock ) ? y.merge(**args) : IB::Stock.new( symbol: y ).merge(**args)} - 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 + # 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{|y| y.is_a?( IB::Stock ) ? y.merge(**args) : IB::Stock.new( symbol: y ).merge(**args)} + 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 - end - end + end + end - def the_description spread - info= spread.legs.map( &:symbol ).zip(spread.combo_legs.map( &:weight )) - "" + def the_description spread + info= spread.legs.map( &:symbol ).zip(spread.combo_legs.map( &:weight )) + "" - end + end - # always route a order as NonGuaranteed - def order_requirements - { combo_params: ['NonGuaranteed', true] } - end + # always route a order as NonGuaranteed + def order_requirements + { combo_params: ['NonGuaranteed', true] } + end - end # class - end # module + end # class + end # module end # module ib diff --git a/plugins/ib/spread-prototypes/straddle.rb b/plugins/ib/spread-prototypes/straddle.rb index 5bc04aa..ef2b083 100644 --- a/plugins/ib/spread-prototypes/straddle.rb +++ b/plugins/ib/spread-prototypes/straddle.rb @@ -10,18 +10,18 @@ class << self # # Call with # IB::Spread::Straddle.fabricate an_option - def fabricate master + 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 ) + 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 | + 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 + 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 # ----------------------------------------- @@ -31,40 +31,40 @@ def fabricate master # # 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.invariant_attributes - .slice( :currency, :symbol, :exchange) - .merge(defaults) + 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.invariant_attributes + .slice( :currency, :symbol, :exchange) + .merge(defaults) .merge( fields ) puts leg_prototype.attributes - leg_prototype.sec_type = 'FOP' if from.is_a?( IB::Future ) + 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 + 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::Future.next_expiry end def requirements - super.merge strike: "the strike of both options", - expiry: "Expiry expressed as »yyyymm(dd)« (String or Integer)" + super.merge strike: "the strike of both options", + expiry: "Expiry expressed as »yyyymm(dd)« (String or Integer)" end - def the_description spread + def the_description spread my_strike = spread.legs.first.strike - "" - end + "" + end end # class - end # module combo + end # module combo end # module ib diff --git a/plugins/ib/spread-prototypes/strangle.rb b/plugins/ib/spread-prototypes/strangle.rb index 844afad..b7397b3 100644 --- a/plugins/ib/spread-prototypes/strangle.rb +++ b/plugins/ib/spread-prototypes/strangle.rb @@ -12,25 +12,25 @@ class << self # # Call with # IB::Strangle.fabricate an_option, numeric_value - def fabricate master, distance + def fabricate master, distance - flip_right = ->(the_right){ the_right == :put ? :call : :put } + flip_right = ->(the_right){ the_right == :put ? :call : :put } - error "Argument must be an option" unless [:option, :futures_option].include? master.sec_type + 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 - 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 ) ) - error "Initialisation of Legs failed" if the_spread.legs.size != 2 - the_spread.description = the_description( the_spread ) - end - end + initialize_spread( master ) do | the_spread | + the_spread.add_leg master + 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 ) ) + 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 @@ -41,25 +41,25 @@ def fabricate master, distance # # Call with # IB::Strangle.build from: IB::Contract, p: a_value, c: a_value, expiry: yyyymm(dd) - def build from:, **fields - underlying = if from.is_a? IB::Option - fields[:p] = from.strike unless fields.key?(:p) || from.right == :call - fields[:c] = from.strike unless fields.key?(:c) || from.right == :puta - 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 = { :p => fields.delete(:p), :c => fields.delete(:c) } - initialize_spread( underlying ) do | the_spread | + def build from:, **fields + underlying = if from.is_a? IB::Option + fields[:p] = from.strike unless fields.key?(:p) || from.right == :call + fields[:c] = from.strike unless fields.key?(:c) || from.right == :puta + 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 = { :p => fields.delete(:p), :c => fields.delete(:c) } + initialize_spread( underlying ) do | the_spread | leg_prototype = IB::Option.new from.attributes .slice( :currency, :symbol, :exchange) .merge(defaults) @@ -68,10 +68,10 @@ def build from:, **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 + 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 @@ -79,17 +79,17 @@ def defaults 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) )" + 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 + def the_description spread "" - end + end end # class - end # module combo + end # module combo end # module ib diff --git a/plugins/ib/spread-prototypes/vertical.rb b/plugins/ib/spread-prototypes/vertical.rb index 0ac40d2..0adc265 100644 --- a/plugins/ib/spread-prototypes/vertical.rb +++ b/plugins/ib/spread-prototypes/vertical.rb @@ -12,20 +12,20 @@ class << self # # Call with # IB::Vertical.fabricate an_option, buy: {another_strike}, (or) , :sell{another_strike} - def fabricate master, buy: 0, sell: 0 + 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? + 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 | + 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 + 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 @@ -36,48 +36,48 @@ def fabricate master, buy: 0, sell: 0 # # 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) + 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 + 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 + right: :put end - def the_description spread - x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:strike )].transpose - "" - end - end # class - end # module vertical + 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 index 92a8f70..904a2ad 100644 --- a/plugins/ib/symbols.rb +++ b/plugins/ib/symbols.rb @@ -15,42 +15,42 @@ # # Symbol Allocations are organized as modules. They represent the contents of yaml files in # -# /lib/symbols/ +# /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. +# 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 +# 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' ) +# IB::Symbols::Name.add_contract :wfc, IB::Stock.new( symbol: 'WFC' ) # -# adds the contract and stores it in the yaml file +# adds the contract and stores it in the yaml file # -# IB::Symbols::Name.wfc # or IB::Symbols::Name[:wfc] +# IB::Symbols::Name.wfc # or IB::Symbols::Name[:wfc] # -# retrieves the contract +# retrieves the contract # -# IB::Symbols::Name.all +# IB::Symbols::Name.all # -# returns an Array of stored contracts +# returns an Array of stored contracts # -# IB::Symbols::Name.remove_contract :wfc +# IB::Symbols::Name.remove_contract :wfc # # deletes the contract from the list (and the file) # # To finish the cycle # -# IB::Symbols::Name.purge_collection +# IB::Symbols::Name.purge_collection # -# deletes the file and erases the collection in memory. +# 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 +# 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. @@ -64,46 +64,46 @@ 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 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 + 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 diff --git a/plugins/ib/symbols/abstract.rb b/plugins/ib/symbols/abstract.rb index 43f63e0..5f1caa9 100644 --- a/plugins/ib/symbols/abstract.rb +++ b/plugins/ib/symbols/abstract.rb @@ -1,17 +1,17 @@ module IB - # reopen the contract-class and add yml_file + # 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 + # 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 @@ -25,45 +25,45 @@ module Symbols =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 + @@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. @@ -72,65 +72,65 @@ def purge_collection 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 + 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/cfd.rb b/plugins/ib/symbols/cfd.rb index 304f636..067c70e 100644 --- a/plugins/ib/symbols/cfd.rb +++ b/plugins/ib/symbols/cfd.rb @@ -6,7 +6,7 @@ module CFD extend Symbols def self.contracts - @contracts.presence || super.merge( + @contracts.presence || super.merge( :dax => IB::Contract.new(:symbol => "IBDE30", sec_type: :cfd, :currency => "EUR", :description => "DAX CFD."), diff --git a/plugins/ib/symbols/combo.rb b/plugins/ib/symbols/combo.rb index 54c32fe..084c343 100644 --- a/plugins/ib/symbols/combo.rb +++ b/plugins/ib/symbols/combo.rb @@ -5,9 +5,9 @@ module Symbols module Combo extend Symbols - def self.contracts + def self.contracts - @contracts ||= { #super.merge( + @contracts ||= { #super.merge( stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: 5000, expiry: IB::Option.next_expiry, trading_class: 'OESX' ) , stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: 5000, back: '2m' , @@ -20,35 +20,35 @@ def self.contracts 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]' + [ 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', 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' - ), + [ 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' - ), - wti_coil: Bag.new( symbol: 'WTI', currency: 'USD', exchange: 'SMART', combo_legs: - [ ComboLeg.new( con_id: 55928698, action: :buy, exchange: 'IPE', ratio: 1), # WTI future June 2017 - ComboLeg.new( con_id: 55850663, action: :sell, exchange: 'IPE', ratio: 1 ) ], # COIL future June 2017 - description: 'Smart Future Spread WTI - COIL (June 2017) ' - ), - wti_brent: Bag.new( symbol: 'CL.BZ', currency: 'USD', exchange: 'NYMEX', combo_legs: - [ ComboLeg.new( con_id: 47207310, action: :buy, exchange: 'NYMEX', ratio: 1), # CL Dec'16 @NYMEX - ComboLeg.new( con_id: 47195961, action: :sell, exchange: 'NYMEX', ratio: 1 ) ], #BZ Dec'16 @NYMEX - description: ' WTI - Brent Spread (Dez. 2016)' - ) - } - # ) - end + 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' + ), + wti_coil: Bag.new( symbol: 'WTI', currency: 'USD', exchange: 'SMART', combo_legs: + [ ComboLeg.new( con_id: 55928698, action: :buy, exchange: 'IPE', ratio: 1), # WTI future June 2017 + ComboLeg.new( con_id: 55850663, action: :sell, exchange: 'IPE', ratio: 1 ) ], # COIL future June 2017 + description: 'Smart Future Spread WTI - COIL (June 2017) ' + ), + wti_brent: Bag.new( symbol: 'CL.BZ', currency: 'USD', exchange: 'NYMEX', combo_legs: + [ ComboLeg.new( con_id: 47207310, action: :buy, exchange: 'NYMEX', ratio: 1), # CL Dec'16 @NYMEX + ComboLeg.new( con_id: 47195961, action: :sell, exchange: 'NYMEX', ratio: 1 ) ], #BZ Dec'16 @NYMEX + description: ' WTI - Brent Spread (Dez. 2016)' + ) + } + # ) + end - end - end + end + end end diff --git a/plugins/ib/symbols/commodity.rb b/plugins/ib/symbols/commodity.rb index b7b52ce..eedc980 100644 --- a/plugins/ib/symbols/commodity.rb +++ b/plugins/ib/symbols/commodity.rb @@ -6,10 +6,10 @@ module Commodity extend Symbols def self.contracts - @contracts.presence || super.merge( + @contracts.presence || super.merge( :xau => IB::Contract.new( symbol: 'XAUUSD', sec_type: :commodity, currency: 'USD', :description => "London Gold ") - ) + ) end end diff --git a/plugins/ib/symbols/forex.rb b/plugins/ib/symbols/forex.rb index 7d99502..f3bc68c 100644 --- a/plugins/ib/symbols/forex.rb +++ b/plugins/ib/symbols/forex.rb @@ -29,7 +29,7 @@ def self.define_contracts :symbol => pair[0..2], :exchange => "IDEALPRO", :currency => pair[3..5], - :local_symbol => pair[0..2]+'.'+pair[3..5], + :local_symbol => pair[0..2]+'.'+pair[3..5], :description => pair ) end diff --git a/plugins/ib/symbols/futures.rb b/plugins/ib/symbols/futures.rb index 521fa22..2adea11 100644 --- a/plugins/ib/symbols/futures.rb +++ b/plugins/ib/symbols/futures.rb @@ -9,7 +9,7 @@ module Futures def self.contracts - @contracts.presence ||( super.merge :ym => IB::Future.new(:symbol => "YM", + @contracts.presence ||( super.merge :ym => IB::Future.new(:symbol => "YM", :expiry => IB::Future.next_expiry, :exchange => "CBOT", :currency => "USD", @@ -62,26 +62,26 @@ def self.contracts multiplier: 1000, exchange: 'CBOT', description: 'US Treasury Note -- 30 Years'), - :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 EuroStoxx 50 -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 EuroStoxx 50 -Future'), :gbp => IB::Future.new(:symbol => "GBP", :expiry => IB::Future.next_expiry, :exchange => "CME", diff --git a/plugins/ib/symbols/index.rb b/plugins/ib/symbols/index.rb index cac4348..efac8f9 100644 --- a/plugins/ib/symbols/index.rb +++ b/plugins/ib/symbols/index.rb @@ -5,37 +5,37 @@ module Index extend Symbols def self.contracts - @contracts.presence || super.merge( - :dax => IB::Index.new(:symbol => "DAX", :currency => "EUR", exchange: 'EUREX', + @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" ), - :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', + :asx => IB::Index.new( :symbol => 'AP', :currency => 'AUD', exchange: 'ASX', + :description => "ASX 200 Index" ), + :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', + :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', + :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', + :vdax => IB::Index.new(:symbol => "VDAX", exchange: 'EUREX', :description => "German VDAX Volatility Index"), - :vix => IB::Index.new(:symbol => "VIX", exchange: 'CBOE', + :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") ) + :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 diff --git a/plugins/ib/symbols/options.rb b/plugins/ib/symbols/options.rb index 2926bc8..c10d44e 100644 --- a/plugins/ib/symbols/options.rb +++ b/plugins/ib/symbols/options.rb @@ -6,7 +6,7 @@ module Symbols module Options extend Symbols - ## usage: IB::Symbols::Options.stoxx.merge( strike: 5000, expiry: 202404 ) + ## 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 @@ -38,59 +38,59 @@ def self.contracts currency: 'USD', exchange: 'SMART', description: "Daily settled Mini-SPX options"), - :spy => IB::Option.new( :symbol => :SPY, + :spy => IB::Option.new( :symbol => :SPY, :expiry => IB::Option.next_expiry, :right => :put, :currency => "USD", - :exchange => 'SMART', + :exchange => 'SMART', :description => "SPY Put next expiration"), - :rut => IB::Option.new( :symbol => :RUT, + :rut => IB::Option.new( :symbol => :RUT, :expiry => IB::Option.next_expiry, :right => :put, :currency => "USD", - :exchange => 'SMART', + :exchange => 'SMART', description: "Monthly settled RUT options"), - :rutw => IB::Option.new( :symbol => :RUT, + :rutw => IB::Option.new( :symbol => :RUT, :expiry => IB::Option.next_expiry, :right => :put, :currency => "USD", - :exchange => 'SMART', + :exchange => 'SMART', description: "Weekly settled RUT options"), - :russell => IB::Option.new( :symbol => :RUT, # :russell == :rut ! + :russell => IB::Option.new( :symbol => :RUT, # :russell == :rut ! :expiry => IB::Option.next_expiry, :right => :put, :currency => "USD", - :exchange => 'SMART', + :exchange => 'SMART', description: "Monthly settled RUT options"), - :mini_russell => IB::Option.new( :symbol => :MRUT, + :mini_russell => IB::Option.new( :symbol => :MRUT, :expiry => IB::Option.next_expiry, :right => :put, :currency => "USD", - :exchange => 'SMART', + :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', + :exchange => 'SMART', :currency => 'USD', :description => "Apple Call 130"), - :ibm => IB::Option.new( symbol: 'IBM', + :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', + 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', + description: 'IBM-Option Chain with strike 180'), + :ibm_lazy_strike => IB::Option.new( symbol: 'IBM', right: :put, - exchange: 'SMART', + exchange: 'SMART', expiry: IB::Option.next_expiry, - description: 'IBM-Option Chain ( monthly expiry)') + description: 'IBM-Option Chain ( monthly expiry)') } end end diff --git a/plugins/ib/symbols/stocks.rb b/plugins/ib/symbols/stocks.rb index ead7157..b414a3d 100644 --- a/plugins/ib/symbols/stocks.rb +++ b/plugins/ib/symbols/stocks.rb @@ -7,38 +7,38 @@ module Stocks 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."), + :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', + :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 + description: 'Siemens AG'), + :wrong => IB::Stock.new( :symbol => "QEEUUE", + :exchange => "NYSE", + :currency => "USD", + :description => "Non-existent stock") + ) + end - end - end + end + end end diff --git a/plugins/ib/verify.rb b/plugins/ib/verify.rb index 695f290..e7a7d01 100644 --- a/plugins/ib/verify.rb +++ b/plugins/ib/verify.rb @@ -44,7 +44,7 @@ module Verify # # Returns nil if the contract could not be verified. # - # > s = Stock.new symbol: 'AA' + # > s = Stock.new symbol: 'AA' # => #"AA", :con_id=>0, :right=>"", :include_expired=>false, # :sec_type=>"STK", :currency=>"USD", :exchange=>"SMART"} @@ -87,8 +87,8 @@ def necessary_attributes 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 + 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 # @@ -102,7 +102,7 @@ def verify! self end - private + private # Base method to verify a contract # @@ -156,9 +156,9 @@ def _verify &b # :nodoc: end # subscribe ### send the request ! - # contract_to_be_queried = con_id.present? ? self : query_contract + # 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 + # if contract_to_be_queried.present? # is nil if query_contract fails message_id = ib.send_message :RequestContractData, :contract => query_contract while r = queue.pop @@ -166,7 +166,7 @@ def _verify &b # :nodoc: end ib.unsubscribe a end - received_contracts # return contracts + received_contracts # return contracts end # Generates an IB::Contract with the required attributes to retrieve a unique contract from the TWS @@ -186,7 +186,7 @@ def _verify &b # :nodoc: 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 + ## 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 @@ -198,14 +198,14 @@ def query_contract( invalid_record: true ) # :nodoc: 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 + # 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 + 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 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 9544b0b..4a0271e 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,41 @@ 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 'EUREX' } - 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 'GLOBEX' } + 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 'CBOT' } - 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 'EDGX' } + its( :symbol ) { should eq "WFC" } +# 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 fdcd8de..3c89e72 100644 --- a/spec/ib/connect_spec.rb +++ b/spec/ib/connect_spec.rb @@ -1,12 +1,12 @@ require "main_helper" 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 } + context "A new connection" do + it{ expect( IB::Connection.current ).to be_a IB::Connection } it "has the proper state" do expect( IB::Connection.current.ready? ).to be_truthy @@ -58,7 +58,7 @@ expect( ib.clients ).to be_an Array end - end + end end diff --git a/spec/ib/contracts/butterfly_spec.rb b/spec/ib/contracts/butterfly_spec.rb index 093fa80..8617652 100644 --- a/spec/ib/contracts/butterfly_spec.rb +++ b/spec/ib/contracts/butterfly_spec.rb @@ -2,8 +2,8 @@ require 'order_helper' RSpec.describe "IB::Butterfly" do - before(:all) do - establish_connection + before(:all) do + establish_connection ib = IB::Connection.current ib.activate_plugin 'verify' ib.activate_plugin 'spread-prototypes' @@ -11,40 +11,40 @@ ib.activate_plugin 'symbols' ib.activate_plugin 'market-price' - ib.subscribe( :Alert ){|y| puts y.to_human } - end + ib.subscribe( :Alert ){|y| puts y.to_human } + end after(:all) do - close_connection + close_connection end - let( :the_option ){ IB::Symbols::Options.stoxx.merge( strike: 5000 ) } - let( :the_bag ){ IB::Symbols::Combo::stoxx_butterfly } + 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 )} + 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' + 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, + 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' + 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 } it{ puts subject.as_table } it{ puts subject.contract.as_table } - it_behaves_like 'serialize limit order fields' + 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 index 6633a9f..c698ad7 100644 --- a/spec/ib/contracts/calendar_spec.rb +++ b/spec/ib/contracts/calendar_spec.rb @@ -17,31 +17,31 @@ let ( :the_option ){ IB::Symbols::Options.stoxx.merge strike: 4800, right: :call, trading_class: 'OESX' } - context "initialize with master-option and second expiry" do + 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 + 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: '-1m' - ) } + 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: '-1m' + ) } 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 + 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 + 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 index 00763e5..6156cf6 100644 --- a/spec/ib/contracts/spread_spec.rb +++ b/spec/ib/contracts/spread_spec.rb @@ -2,14 +2,14 @@ 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 } + 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 + 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 @@ -28,7 +28,7 @@ end RSpec.describe "IB::Spread" do - let( :the_option ) { IB::Symbols::Options.stoxx.merge( strike: 5000 ) } + let( :the_option ) { IB::Symbols::Options.stoxx.merge( strike: 5000 ) } let( :the_spread ) { IB::Calendar.fabricate IB::Symbols::Futures.nq, '3m' } before(:all) do @@ -47,47 +47,47 @@ end - context "initialize by fabrication" do + context "initialize by fabrication" do - subject{ the_spread } - it{ is_expected.to be_a IB::Bag } - it_behaves_like 'a valid NQ-FUT Combo' + 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 + 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_behaves_like "serialize limit order fields" + it_behaves_like "serialize two Combo-legs" it { expect( subject.serialize_combo_legs ).to eq [ the_spread.serialize_legs, 0 ,[], 0 , [] ] } # leg-prices + combo-params - end + end - context "leg management" do - subject { the_spread } + context "leg management" do + subject { the_spread } - its( :legs ){ is_expected.to have(2).elements } + 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 "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 } + 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 +# 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/extensions_spec.rb b/spec/ib/extensions_spec.rb index db54894..417efc9 100644 --- a/spec/ib/extensions_spec.rb +++ b/spec/ib/extensions_spec.rb @@ -7,10 +7,10 @@ context "Array-Extensions" do end - context "Time-Extensions" do + context "Time-Extensions" do Given( :the_time ){ Time.now } Then{ the_time.to_ib == the_time.strftime("%Y%m%d %H:%M:%S") } - end + end # Numeric positive Values are true, zero and below is false context "numeric boolean Test" do 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 c2c1f74..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', diff --git a/spec/ib/messages/incoming/abstract_message_spec.rb b/spec/ib/messages/incoming/abstract_message_spec.rb index 0c6c7fb..bd82e70 100644 --- a/spec/ib/messages/incoming/abstract_message_spec.rb +++ b/spec/ib/messages/incoming/abstract_message_spec.rb @@ -3,192 +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 } + 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 99bb787..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,26 +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 df65e70..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,12 +19,12 @@ 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 @@ -34,12 +34,12 @@ 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 835237a..c5b89b5 100644 --- a/spec/ib/messages/incoming/account_update_multi_spec.rb +++ b/spec/ib/messages/incoming/account_update_multi_spec.rb @@ -3,16 +3,16 @@ 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 Numeric } - 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,18 +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 f851b08..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 diff --git a/spec/ib/messages/incoming/contract_data_spec.rb b/spec/ib/messages/incoming/contract_data_spec.rb index 42a5aa2..25bc929 100644 --- a/spec/ib/messages/incoming/contract_data_spec.rb +++ b/spec/ib/messages/incoming/contract_data_spec.rb @@ -4,39 +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.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 :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 + 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 85c1a3c..2ba4924 100644 --- a/spec/ib/messages/incoming/managed_accounts_spec.rb +++ b/spec/ib/messages/incoming/managed_accounts_spec.rb @@ -4,8 +4,8 @@ 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 @@ -19,7 +19,7 @@ context 'Message received from IB', :connected => true do before(:all) do - establish_connection + establish_connection end after(:all) { close_connection } @@ -28,8 +28,8 @@ 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 index 701ef6c..680d53f 100644 --- a/spec/ib/messages/incoming/open_position_spec.rb +++ b/spec/ib/messages/incoming/open_position_spec.rb @@ -1,13 +1,13 @@ require 'main_helper' RSpec.shared_examples 'Open Position Message' do - subject{ the_message } + 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( :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 } + its( :buffer ) { is_expected.to be_empty } it 'has class accessors as well' do expect( subject.class.message_id).to eq 5 @@ -157,20 +157,20 @@ end RSpec.describe IB::Messages::Incoming::OpenOrder do - context "Syntetic Message" do - let( :the_message ) do - IB::Messages::Incoming::OpenOrder.new( + 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 + 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 "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' @@ -183,28 +183,28 @@ 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' + 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 + 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/option_chain_spec.rb b/spec/ib/messages/incoming/option_chain_spec.rb index 9330218..44930bd 100644 --- a/spec/ib/messages/incoming/option_chain_spec.rb +++ b/spec/ib/messages/incoming/option_chain_spec.rb @@ -29,7 +29,7 @@ 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 904e104..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,13 +19,13 @@ # ## 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 diff --git a/spec/ib/messages/incoming/positios_multi_spec.rb b/spec/ib/messages/incoming/positios_multi_spec.rb index 108e259..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 } + 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 eefbc7e..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 @@ -21,8 +21,8 @@ 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 eefbc7e..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 @@ -21,8 +21,8 @@ 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/orders/account_spec.rb b/spec/ib/orders/account_spec.rb index f25c908..de25cc2 100644 --- a/spec/ib/orders/account_spec.rb +++ b/spec/ib/orders/account_spec.rb @@ -1,45 +1,45 @@ require 'order_helper' describe 'Order placement via Account' do # :connected => true, :integration => true do - let(:contract_type) { :stock } + let(:contract_type) { :stock } - before(:all) do + 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 + 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' } # trading hours: 2 - 10 am GMT, min-lot-size: 100 - let( :tui ){IB::Stock.new symbol: :tui1, exchange: :smart, currency: :eur } # trading hours: 2 - 10 am GMT, min-lot-size: 100 + 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' } # trading hours: 2 - 10 am GMT, min-lot-size: 100 + let( :tui ){IB::Stock.new symbol: :tui1, exchange: :smart, currency: :eur } # trading hours: 2 - 10 am GMT, min-lot-size: 100 - let( :the_client ){ IB::Connection.current.clients.detect{|y| y.account == ACCOUNT} } + 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) + 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 + 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( 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 + 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 mp = tui.market_price @@ -53,16 +53,16 @@ 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( 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 + expect( the_client.orders.first.order_states.last.filled).to be_zero end - end - + end + end # describe diff --git a/spec/ib/plugins/auto_adjust_spec.rb b/spec/ib/plugins/auto_adjust_spec.rb index 8cdf626..e37579c 100644 --- a/spec/ib/plugins/auto_adjust_spec.rb +++ b/spec/ib/plugins/auto_adjust_spec.rb @@ -1,7 +1,7 @@ require "main_helper" describe "Connect to TWS and activate Plugin" do - before(:all) do + before(:all) do establish_connection c = IB::Connection.current c.activate_plugin "verify" @@ -11,9 +11,9 @@ after(:all) { close_connection } - context "A new connection is established" do - it{ expect( IB::Connection.current ).to be_a IB::Connection } - end + 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 } diff --git a/spec/ib/plugins/managed_account_spec.rb b/spec/ib/plugins/managed_account_spec.rb index 2c18e34..f2e61dc 100644 --- a/spec/ib/plugins/managed_account_spec.rb +++ b/spec/ib/plugins/managed_account_spec.rb @@ -1,7 +1,7 @@ require "main_helper" describe "Connect to Gateway or TWS" do - before(:all){ establish_connection 'managed-accounts'} + before(:all){ establish_connection 'managed-accounts'} after(:all) { close_connection } diff --git a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb index f07daab..933623f 100644 --- a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -37,7 +37,7 @@ # # subject do # IB::Messages::Outgoing::PlaceOrder.new( -# local_id: 123, +# local_id: 123, # contract: IB::Stock.new( symbol: 'F' ), # order: IB::Order.new( total_quantity: 100, limit_price: 25, tif: :good_til_canceled )) # end @@ -55,22 +55,22 @@ # # # 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields # if subject.server_version < 177 # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields # else # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields ## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields # end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields # # end # diff --git a/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb index 5c1816c..ec08276 100644 --- a/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb @@ -56,7 +56,7 @@ # # subject do # IB::Messages::Outgoing::PlaceOrder.new( -# local_id: 123, +# local_id: 123, # contract: IB::Stock.new( symbol: 'F' ), # order: IB::Order.new( total_quantity: 100, limit_price: 25, tif: :good_til_canceled )) # end @@ -74,22 +74,22 @@ # # # 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields # if subject.server_version < 177 # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields # else # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields ## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields # end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields # # end # diff --git a/spec/ib/plugins/order-prototypes/limit_order_spec.rb b/spec/ib/plugins/order-prototypes/limit_order_spec.rb index 9a81cb6..febcc9b 100644 --- a/spec/ib/plugins/order-prototypes/limit_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -40,7 +40,7 @@ # # subject do # IB::Messages::Outgoing::PlaceOrder.new( -# local_id: 123, +# local_id: 123, # contract: IB::Stock.new( symbol: 'F' ), # order: IB::Order.new( total_quantity: 100, limit_price: 25, tif: :good_til_canceled )) # end @@ -58,22 +58,22 @@ # # # 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields +# 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 +## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields # if subject.server_version < 177 # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields # else # expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields ## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields # end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields +## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields +## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- +## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields +## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields # # end # diff --git a/spec/ib/plugins/verify_spec.rb b/spec/ib/plugins/verify_spec.rb index 456730a..e9ef23f 100644 --- a/spec/ib/plugins/verify_spec.rb +++ b/spec/ib/plugins/verify_spec.rb @@ -1,13 +1,13 @@ require "main_helper" 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 + it{ expect( IB::Connection.current ).to be_a IB::Connection } + end # context "Plugin not present" do # Given( :current ){ IB::Connection.current } diff --git a/spec/ib/stock_spec.rb b/spec/ib/stock_spec.rb index c20cc09..ddd74ce 100644 --- a/spec/ib/stock_spec.rb +++ b/spec/ib/stock_spec.rb @@ -1,7 +1,7 @@ require "main_helper" describe IB::Stock do - before(:all) do + before(:all) do establish_connection ib = IB::Connection.current ib.activate_plugin 'verify' diff --git a/spec/main_helper.rb b/spec/main_helper.rb index 6e0c9f9..dc3d1a4 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -40,7 +40,7 @@ def establish_connection *plugins accounts = nil if plugins.map( &:to_s ).include?("managed-accounts") || plugins.include?("process-orders") || plugins.include?('gateway') OPTS[:connection].merge connect: false - ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) + ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) ib.activate_plugin 'verify', 'process-orders', 'advanced-account' ib.received = true ib.get_account_data @@ -51,23 +51,23 @@ def establish_connection *plugins ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) ib.received = true ib.try_connection! - ib.wait_for :ManagedAccounts, 5 + ib.wait_for :ManagedAccounts, 5 - raise "Unable to verify IB PAPER ACCOUNT" unless ib.received?(:ManagedAccounts) + raise "Unable to verify IB PAPER ACCOUNT" unless ib.received?(:ManagedAccounts) - accounts = ib.received[:ManagedAccounts].first.accounts_list.split(',') + accounts = ib.received[:ManagedAccounts].first.accounts_list.split(',') end if ib - unless accounts.include?(ACCOUNT) - close_connection + 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 + puts "Performing tests with ClientId: #{ib.client_id}" + OPTS[:account_verified] = true + else + OPTS[:account_verified] = false + raise "could not establish connection!" + end end @@ -75,18 +75,18 @@ def establish_connection *plugins # 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 - clean_connection + 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 16ee52d..2b879a1 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -3,52 +3,52 @@ 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 + 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 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 @@ -58,7 +58,7 @@ def remove_open_orders 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 @@ -200,7 +200,7 @@ 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 @@ -212,33 +212,33 @@ 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 + if pnl>0 + its( :realized_pnl ){is_expected.to be_a BigDecimal} + else + its( :realized_pnl ){is_expected.to be_nil} + end + + end =begin @@ -250,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 25f25a1..e9f5861 100644 --- a/spec/spec.yml +++ b/spec/spec.yml @@ -1,13 +1,12 @@ --- :connection: - :port: 4002 # 7497 or 4001 / 7496 + :port: 7497 # 4002 # 7497 or 4001 / 7496 :host: 127.0.0.1 - # :port: 7496 # 4002 # 7497 or 4001 / 7496 - #:host: 10.247.8.109 #10.247.8.109 # 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: DU4035278 # 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: # SAMPLE Stock in tests :symbol: 'GE' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5ef2f3f..d1aedfc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,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 @@ -31,22 +31,22 @@ 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 From 5064701e9081045ff24feec4b7e5cb9568e77415 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Wed, 24 Jul 2024 09:51:41 +0200 Subject: [PATCH 55/76] Order#place: ensure that only con_di and exchange are transmitted if con_id is presen Account#place: more robust implemenation of auto_adjust --- models/ib/order.rb | 10 +++-- plugins/ib/advanced-account.rb | 74 +++++++++++++++++++++++----------- plugins/ib/auto-adjust.rb | 18 +++------ spec/ib/orders/account_spec.rb | 9 ++--- 4 files changed, 67 insertions(+), 44 deletions(-) diff --git a/models/ib/order.rb b/models/ib/order.rb index 45aa0c7..fce3e5d 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -651,13 +651,15 @@ def place the_contract=nil, connection=nil # 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? + the_contract = contract if the_contract.nil? + error "Unable to place order, contract has to be specified" unless the_contract.is_a?( IB::Contract ) + connection ||= IB::Connection.current self.modified_at = time connection.send_message :PlaceOrder, - :order => self, - :contract => contract, - :local_id => local_id + :order => self, + :contract => the_contract.con_id.to_i > 0 ? Contract.new( con_id: the_contract.con_id, exchange: the_contract.exchange ) : the_contract, + :local_id => local_id local_id end diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 8f88837..a3b470e 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -85,7 +85,7 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ j36 = IB::Stock.new symbol: 'J36', exchange: 'SGX' order = IB::Limit.order size: 100, price: 65.5 - g = IB::Gateway.current.clients.last + g = IB::Connection.current.clients.last g.preview contract: j36, order: order => {:init_margin=>0.10864874e6, @@ -95,10 +95,9 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ :commission_currency=>"USD", :warning=>"" - the_local_id = g.place order: order + g.place order: order => 67 # returns local_id - order.contract # updated contract-record - + order.contract # updated (and verifired) contract-record => #9534669, :exchange=>"SGX", :right=>"", @@ -108,11 +107,11 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ g.modify order: order # and transmit => 67 # returns local_id - g.locate_order( local_id: the_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> + 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 @@ -131,6 +130,8 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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) @@ -147,7 +148,13 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true ### Default action: raise IB::Transmission Error sa = ib.subscribe( :Alert ) do | msg | if msg.error_id == the_local_id - if [ 110, # The price does not confirm to the minimum price variation for this contract + + if msg.code == 110 && auto_adjust + wrong_order = nil + the_local_id = -1 + ib.logger.warn "adjusting order-price" + q.close + elsif [ 110, # The price does not confirm to the minimum price variation for this contract 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 @@ -176,25 +183,46 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank? # 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 - the_contract = order.contract.con_id.to_i > 0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil - the_local_id = order.place the_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 + # ... 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 + loop do + the_local_id = order.place # 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 + order.contract.contract_detail.min_tick + else + p - order.contract.contract_detail.min_tick + end + end - ib.unsubscribe sa - ib.unsubscribe sb - if q.closed? - if wrong_order.present? - raise IB::SymbolError, wrong_order - elsif the_local_id.present? - order.local_id = the_local_id + puts "the_loclal_id: #{the_local_id}" + puts "order_id: #{order.local_id}" + 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 - error " #{order.to_human} is not transmitted properly", :symbol + order=tws_answer # return order-record received from tws end - else - order=tws_answer # return order-record received from tws + break unless order.local_id.nil? + q = Queue.new # reset queue end + ib.unsubscribe sa + ib.unsubscribe sb the_local_id # return_value end # place diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb index 5cb4f98..5bfa94d 100644 --- a/plugins/ib/auto-adjust.rb +++ b/plugins/ib/auto-adjust.rb @@ -18,11 +18,11 @@ module IB Standard usage ```ruby -c = IB::Stock.new symbol = 'GE' +c = IB::Stock.new symbol 'GE' o = IB::Limit.order contract: c, price: 150.0998, size: 100 o.auto_adjust -o.limit_price => 151 +o.limit_price => 151.1 ``` =end @@ -32,15 +32,10 @@ module AutoAdjust # Auto Adjust implements a simple algorithm to ensure that an order is accepted # It reads `contract_detail.min_tick`. - # # - # If min_tick < 0.01, the real tick-increments differ fron the min_tick_value # - # For J36 (jardines) min tick is 0.001, but the minimal increment is 0.005 - # For Tui1 its the samme, min_tick is 0.00001 , minimal increment ist 0.00005 + # For min-tick smaller then 0.01, the value is rounded to the next higer digit. # - # Thus, for min-tick smaller then 0.01, the value is rounded to the next higer digit. - # - # ATTENTION: The method mutates the Order-Object. + # The method mutates the Order-Object. # # | min-tick | round | # |--------------|------------| @@ -65,13 +60,12 @@ def auto_adjust error "No Contract provided to Auto adjust" unless contract.is_a? IB::Contract unless contract.is_a? IB::Bag - # ensure that contract_details are present 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_d.zero? - self.aux_price= adjust_price.call(aux_price.to_d, min_tick) unless aux_price.to_d.zero? + 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 diff --git a/spec/ib/orders/account_spec.rb b/spec/ib/orders/account_spec.rb index de25cc2..c4e1370 100644 --- a/spec/ib/orders/account_spec.rb +++ b/spec/ib/orders/account_spec.rb @@ -15,9 +15,8 @@ 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' } # trading hours: 2 - 10 am GMT, min-lot-size: 100 - let( :tui ){IB::Stock.new symbol: :tui1, exchange: :smart, currency: :eur } # 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 @@ -42,11 +41,11 @@ 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}" - puts "the_price: #{the_price.class}" + 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, From ebf4caa233a38c23739984e0883a2b836849d550 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 25 Jul 2024 06:34:12 +0200 Subject: [PATCH 56/76] removing debug related outputs --- plugins/ib/advanced-account.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index a3b470e..7dd0b26 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -199,8 +199,6 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true end end - puts "the_loclal_id: #{the_local_id}" - puts "order_id: #{order.local_id}" if q.closed? if wrong_order.present? raise IB::SymbolError, wrong_order From cbd26f21b40155c5a1ee03b6cf498c4faccf24e7 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 2 Aug 2024 07:11:00 +0200 Subject: [PATCH 57/76] Fixing fetching market_price plugin for spreads and complex contracts --- lib/ib-api.rb | 1 + .../messages/outgoing/request_market_data.rb | 18 +-- models/ib/bag.rb | 36 ++--- models/ib/contract.rb | 4 +- models/ib/order.rb | 41 +----- plugins/ib/advanced-account.rb | 63 ++++++--- plugins/ib/managed-accounts.rb | 5 + plugins/ib/market-price.rb | 60 ++++++--- plugins/ib/order-flow.rb | 127 ++++++++++++++++++ plugins/ib/symbols/combo.rb | 6 +- spec/main_helper.rb | 11 +- spec/order_helper.rb | 25 ++-- 12 files changed, 276 insertions(+), 121 deletions(-) create mode 100644 plugins/ib/order-flow.rb diff --git a/lib/ib-api.rb b/lib/ib-api.rb index b6a28ca..a523a4c 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -33,6 +33,7 @@ ) #loader.push_dir("#{__dir__}") loader.push_dir("#{__dir__}/../models/") +loader.push_dir("#{__dir__}/../conditions/") loader.setup loader.eager_load #require 'requires' diff --git a/lib/ib/messages/outgoing/request_market_data.rb b/lib/ib/messages/outgoing/request_market_data.rb index 77c4958..bdf1f27 100644 --- a/lib/ib/messages/outgoing/request_market_data.rb +++ b/lib/ib/messages/outgoing/request_market_data.rb @@ -4,21 +4,21 @@ module Outgoing extend Messages # def_message macros 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, ""] # changed to enable requests in V 10.19 ff + 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 diff --git a/models/ib/bag.rb b/models/ib/bag.rb index 9f95246..7513b20 100644 --- a/models/ib/bag.rb +++ b/models/ib/bag.rb @@ -44,22 +44,26 @@ def same_legs? other legs_description.split(',').sort == other.legs_description.split(',').sort end - def serialize_legs - [ combo_legs.size, - combo_legs.map do |the_leg| - [ - the_leg.con_id, - the_leg.ratio, - the_leg.side.to_sup, - the_leg.exchange, - the_leg[:open_close], - the_leg[:short_sale_slot], - the_leg.designated_location, - the_leg.exempt_code - ] - end - ] - end +# def serialize_legs dest = :order +# unless dest == :order +# super +# else +# [ combo_legs.size, +# combo_legs.map do |the_leg| +# [ +# the_leg.con_id, +# the_leg.ratio, +# the_leg.side.to_sup, +# the_leg.exchange, +# the_leg[:open_close], +# the_leg[:short_sale_slot], +# the_leg.designated_location, +# the_leg.exempt_code +# ] +# end +# ] +# end +# end # Contract comparison def == other diff --git a/models/ib/contract.rb b/models/ib/contract.rb index 88720a2..9fd2a78 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -173,14 +173,14 @@ def serialize_under_comp *args # :nodoc: 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( &:serialize ) ] end end diff --git a/models/ib/order.rb b/models/ib/order.rb index fce3e5d..fe8592b 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -306,13 +306,13 @@ class Order < IB::Base # 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 has_many :conditions @@ -632,37 +632,6 @@ def serialize_peg_best_and_mid def serialize_misc_options "" # Vers. 70 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 - 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? - the_contract = contract if the_contract.nil? - error "Unable to place order, contract has to be specified" unless the_contract.is_a?( IB::Contract ) - - connection ||= IB::Connection.current - self.modified_at = time - connection.send_message :PlaceOrder, - :order => self, - :contract => the_contract.con_id.to_i > 0 ? Contract.new( con_id: the_contract.con_id, exchange: the_contract.exchange ) : the_contract, - :local_id => local_id - local_id - end - # Order comparison def == other super(other) || @@ -711,8 +680,8 @@ def to_human end - def table_header - [ 'account','status' ,'', 'Type', 'tif', 'action', 'amount','price' , 'misc' ] + def table_header + [ 'account','status', '', 'Type', 'tif', 'action', 'amount','price' , 'misc' ] end def table_row @@ -729,7 +698,7 @@ def table_row self[:tif], action, total_quantity, - (limit_price ? "#{limit_price} " : '') + ((aux_price && aux_price != 0) ? "/#{aux_price}" : '') , + ((limit_price && !limit_price.zero?) ? "#{limit_price} " : '') + ((aux_price && !aux_price.zero?) ? "/#{aux_price}" : '') , misc.join( " " ) ] end diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 7dd0b26..ecb453b 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -148,24 +148,25 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true ### Default action: raise IB::Transmission Error sa = ib.subscribe( :Alert ) do | msg | if msg.error_id == the_local_id - - if msg.code == 110 && auto_adjust - wrong_order = nil - the_local_id = -1 - ib.logger.warn "adjusting order-price" - q.close - elsif [ 110, # The price does not confirm to the minimum price variation for this contract - 201, # Order rejected, No Trading permissions + 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.warn "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 - wrong_order = msg.message ib.logger.error msg.message - q.close # closing the queue indicates that no order was transmitted end + q.close # closing the queue indicates that no order was transmitted end end # transfer the received openOrder to the queue @@ -221,7 +222,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true end ib.unsubscribe sa ib.unsubscribe sb - the_local_id # return_value + self # return_value end # place @@ -267,9 +268,27 @@ def modify_order local_id: nil, order_ref: nil, order:nil, contract: nil # # Submits a "WhatIf" Order # - # Returns the order_state.forecast + # Returns the presubmiited order record, where the local_id is erased and the what_if-flag is false # - # The order received from the TWS is kept in account.orders + # 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 # @@ -280,7 +299,7 @@ def preview order:, contract: nil, **args_which_are_ignored 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.init_margin.nil? + q << m.order if m.order.local_id.to_i == the_local_id.to_i #&& !m.order.init_margin.nil? end order.what_if = true @@ -289,12 +308,16 @@ def preview order:, contract: nil, **args_which_are_ignored 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 +# 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? - returned_order.order_state.forcast # return_value + #order.order_state.forcast # return_value + returned_order.local_id = nil + returned_order.what_if = false + returned_order end + # closes the contract by submitting an appropriate order # the action- and total_amount attributes of the assigned order are overwritten. # @@ -305,8 +328,7 @@ def preview order:, contract: nil, **args_which_are_ignored # Any value in total_quantity is overwritten # # returns the order transmitted - # - # raises an IB::Error if no PortfolioValues have been loaded to the IB::Account + #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 @@ -318,7 +340,7 @@ def close order:, contract: nil, reverse: false, **args_which_are_ignored end end - order.contract = contract.verify.first unless contract.nil? + 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 @@ -409,6 +431,7 @@ def complex_position con_id # # 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 diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index 956170f..65344a7 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -236,5 +236,10 @@ def subscribe_account_updates continuously: true 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 index ab2cccd..d72d58c 100644 --- a/plugins/ib/market-price.rb +++ b/plugins/ib/market-price.rb @@ -1,31 +1,49 @@ 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 `place_order`. -# -# The result can be customized by a provided block. -# -# IB::Symbols::Stocks.sie.market_price{ |x| x } -# -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3} + # Ask for the Market-Price # + # For valid contracts, either bid/ask or last_price and close_price are transmitted. # - # Raw-data are stored in the _bars_-attribute of IB::Contract - # (volatile, ie. data are not preserved when the Object is copied) + # If last_price is received, its returned. + # If not, midpoint (bid+ask/2) is used. Else the closing price will be returned. # - #Example: IB::Stock.new(symbol: :ge).market_price - # returns the current market-price + # 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. # - #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join - # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message - # and returns this value, 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 diff --git a/plugins/ib/order-flow.rb b/plugins/ib/order-flow.rb new file mode 100644 index 0000000..fb37c7d --- /dev/null +++ b/plugins/ib/order-flow.rb @@ -0,0 +1,127 @@ +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 the_contract=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, self.placed_at + self + end + + # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns local_id. + def modify the_contract=nil, time=Time.now + error "Unable to modify order; local_id not specified" if local_id.nil? + the_contract = contract if the_contract.nil? + error "Unable to place order, contract has to be specified" unless the_contract.is_a?( IB::Contract ) + + connection = IB::Connection.current + self.modified_at = time + connection.send_message :PlaceOrder, + :order => self, + :contract => if the_contract.con_id.to_i > 0 + Contract.new con_id: the_contract.con_id, + exchange: the_contract.exchange + else + the_contract + end + :local_id => local_id + local_id # return the local_id + end + + # returns the order if the margin-requirements are met + # + # 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 + # ``` + def check_margin treshold = 0.1 + error "Unable to check margin, forcast is not initialized" if order_state.nil or order_state.forecast[ :init_margin ].nil? + ib = Connection.current + client = ib.clients.find{|y| y.account == account} + net_liquidation = client.account_data_scan( /NetLiquidation$/ ).first.value.to_i + buffer = order_state.forcast.then{ |x| x[ :equity_with_loan ] - x[ :init_margin ] } + buffer > net_liquidation * treshold ? self : nil + 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/symbols/combo.rb b/plugins/ib/symbols/combo.rb index 084c343..e72ccc3 100644 --- a/plugins/ib/symbols/combo.rb +++ b/plugins/ib/symbols/combo.rb @@ -24,9 +24,9 @@ def self.contracts 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', combo_legs: - [ ComboLeg.new( con_id: 43645865, action: :buy, ratio: 1), # IKBR STK - ComboLeg.new( con_id: 9408, action: :sell,ratio: 1 ) ], # MCD STK + 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' ), diff --git a/spec/main_helper.rb b/spec/main_helper.rb index dc3d1a4..a9d97be 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -34,7 +34,12 @@ def should_not_log *patterns end -## Connection helpers +## 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 @@ -42,14 +47,14 @@ def establish_connection *plugins OPTS[:connection].merge connect: false ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) ib.activate_plugin 'verify', 'process-orders', 'advanced-account' - ib.received = true 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.received = true ib.try_connection! ib.wait_for :ManagedAccounts, 5 diff --git a/spec/order_helper.rb b/spec/order_helper.rb index 2b879a1..56a6f17 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -9,18 +9,21 @@ 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? +def place_the_order( contract: IB::Symbols::Stocks.wfc ) 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 + connection = IB::Connection.current + local_id = order.local_id || connection.next_local_id + + connection.send_message :PlaceOrder, :order => order, + :contract => if contract.con_id.to_i > 0 + Contract.new con_id: contract.con_id, + exchange: the_contract.exchange + else + contract + end + :local_id => local_id + IB::Connection.current.wait_for :OpenOrder, 3 + local_id # return value end def get_contract_price contract: IB::Symbols::Stocks.wfc From ab5b27d4114275a2645cb1f9368145461cf491ea Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 2 Aug 2024 08:10:37 +0200 Subject: [PATCH 58/76] Fixing Test for OptenPosition Message --- .../messages/incoming/open_position_spec.rb | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/spec/ib/messages/incoming/open_position_spec.rb b/spec/ib/messages/incoming/open_position_spec.rb index 680d53f..ee86dcf 100644 --- a/spec/ib/messages/incoming/open_position_spec.rb +++ b/spec/ib/messages/incoming/open_position_spec.rb @@ -30,8 +30,8 @@ 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 false } - its( :hidden ) { is_expected.to be false } + 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 } @@ -40,31 +40,31 @@ its( :fa_profile ) { is_expected.to be_empty } its( :model_code ) { is_expected.to be_empty } - its( :rule_80a ) { is_expected.to be_nil } ### todo : empty?? - its( :percent_offset ) { is_expected.to be_nil } ### todo : zero?? + 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_nil } - its( :delta ) { is_expected.to be_nil } - its( :stock_range_lower ) { is_expected.to be_nil } - its( :stock_range_upper ) { 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 false } - its( :sweep_to_fill ) { is_expected.to be false } - its( :all_or_none ) { is_expected.to be false } - its( :min_quantity ) { is_expected.to be_nil } + 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 false } + 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 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 @@ -76,8 +76,8 @@ 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_nil } - its( :trailing_percent ) { 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 } @@ -105,7 +105,7 @@ 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_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 } From 11ce517fdbe5b88f40d14e88ccb8ac3111cf6171 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 22 Oct 2024 20:27:45 +0200 Subject: [PATCH 59/76] Transfer to new host --- lib/ib/connection.rb | 7 +- lib/ib/messages/outgoing/place_order.rb | 3 +- models/ib/order.rb | 8 +- plugins/ib/advanced-account.rb | 21 ++++-- plugins/ib/auto-adjust.rb | 75 ------------------- plugins/ib/order-flow.rb | 73 +++++++++++------- plugins/ib/process-orders.rb | 2 +- plugins/ib/symbols/combo.rb | 50 +++++-------- plugins/ib/symbols/futures.rb | 24 ++++-- plugins/ib/symbols/index.rb | 40 +++++----- spec/ib/contracts/butterfly_spec.rb | 3 +- .../ib/messages/outgoing/account_data_spec.rb | 40 ---------- spec/main_helper.rb | 2 +- spec/order_helper.rb | 41 +++++----- 14 files changed, 149 insertions(+), 240 deletions(-) delete mode 100644 spec/ib/messages/outgoing/account_data_spec.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index edced6a..68dfba2 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -402,13 +402,12 @@ def place_order order, contract # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns order_id. def modify_order order, contract - puts "contract: #{contract.to_human}" - # order.modify contract, self ## old 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 diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index 236af8a..b698036 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -9,6 +9,7 @@ class PlaceOrder def encode order = @data[:order] contract = @data[:contract] + contract = order.contract unless contract.is_a? IB::Contract error 'contract has to be specified' unless contract.is_a? IB::Contract @@ -18,7 +19,7 @@ def encode order.serialize_main_order_fields, order.serialize_extended_order_fields, order.serialize_combo_legs(contract), - order.serialize_auxilery_order_fields # incluing advisory order fields + order.serialize_auxilery_order_fields # including advisory order fields ] if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] # 103 diff --git a/models/ib/order.rb b/models/ib/order.rb index fe8592b..f865860 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -679,9 +679,11 @@ def to_human (misc.empty? ? "" : " ") + misc.join( " " ) + ">" end + alias inspect to_human + def table_header - [ 'account','status', '', 'Type', 'tif', 'action', 'amount','price' , 'misc' ] + [ 'account','status', 'contract', 'type', 'tif', 'action', 'amount','price' , '','misc' ] end def table_row @@ -693,12 +695,12 @@ def table_row 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[1..-2], + 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.zero?) ? "#{limit_price} " : '') + ((aux_price && !aux_price.zero?) ? "/#{aux_price}" : '') , + ((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 diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index ecb453b..9f7d9d2 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -115,7 +115,6 @@ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/ =end def place_order order:, contract: nil, auto_adjust: true, convert_size: true - # adjust the order price to min-tick result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } } qualified_contract = ->(c) do c.is_a?(IB::Contract) && @@ -136,7 +135,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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] + error "place order: ContractVerification failed. No con_id assigned" unless qualified_contract[order.contract] or contract.nil? # declare some variables ib = IB::Connection.current @@ -145,7 +144,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true q = Queue.new ### Handle Error messages - ### Default action: raise IB::Transmission Error + ### 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 @@ -186,17 +185,19 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true # 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 = order.place # return the local_id + 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 + order.contract.contract_detail.min_tick + p + contract.contract_detail.min_tick else - p - order.contract.contract_detail.min_tick + p - contract.contract_detail.min_tick end end @@ -220,9 +221,10 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true break unless order.local_id.nil? q = Queue.new # reset queue end + order.contract = contract ib.unsubscribe sa ib.unsubscribe sb - self # return_value + order # return the order-record end # place @@ -296,6 +298,8 @@ 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| @@ -304,7 +308,7 @@ def preview order:, contract: nil, **args_which_are_ignored order.what_if = true order.account = account - the_local_id = order.place contract + 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 @@ -314,6 +318,7 @@ def preview order:, contract: nil, **args_which_are_ignored #order.order_state.forcast # return_value returned_order.local_id = nil returned_order.what_if = false + returned_order.contract = contract returned_order end diff --git a/plugins/ib/auto-adjust.rb b/plugins/ib/auto-adjust.rb index 5bfa94d..e69de29 100644 --- a/plugins/ib/auto-adjust.rb +++ b/plugins/ib/auto-adjust.rb @@ -1,75 +0,0 @@ -module IB -=begin - -Plugin that provides helper methods for orders - -Requires activation of the `verify`-Plugin - -Extends IB::Order - -Changes the IB::Order-object - - -Public API -========== - -* auto_adjust - -Standard usage - -```ruby -c = IB::Stock.new symbol 'GE' -o = IB::Limit.order contract: c, price: 150.0998, size: 100 -o.auto_adjust - -o.limit_price => 151.1 - -``` -=end - - module AutoAdjust - - # 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 - class Order - include AutoAdjust - end # class Order -end # module diff --git a/plugins/ib/order-flow.rb b/plugins/ib/order-flow.rb index fb37c7d..0dc793b 100644 --- a/plugins/ib/order-flow.rb +++ b/plugins/ib/order-flow.rb @@ -27,36 +27,57 @@ module OrderFlow # 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 + 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.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, self.placed_at - self + #connection.place_order self.dup.then{|y| y.contract = nil; y}, contract + modify end - # Modify Order (convenience wrapper for send_message :PlaceOrder). Returns local_id. - def modify the_contract=nil, time=Time.now + # 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? - the_contract = contract if the_contract.nil? - error "Unable to place order, contract has to be specified" unless the_contract.is_a?( IB::Contract ) + 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 - connection = IB::Connection.current - self.modified_at = time - connection.send_message :PlaceOrder, - :order => self, - :contract => if the_contract.con_id.to_i > 0 - Contract.new con_id: the_contract.con_id, - exchange: the_contract.exchange - else - the_contract - end - :local_id => local_id - local_id # return the local_id + 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 @@ -71,10 +92,10 @@ def modify the_contract=nil, time=Time.now def check_margin treshold = 0.1 error "Unable to check margin, forcast is not initialized" if order_state.nil or order_state.forecast[ :init_margin ].nil? ib = Connection.current - client = ib.clients.find{|y| y.account == account} - net_liquidation = client.account_data_scan( /NetLiquidation$/ ).first.value.to_i +# client = ib.clients.find{|y| y.account == account} +# net_liquidation = client.account_data_scan( /NetLiquidation$/ ).first.value.to_i buffer = order_state.forcast.then{ |x| x[ :equity_with_loan ] - x[ :init_margin ] } - buffer > net_liquidation * treshold ? self : nil + buffer > order_state.forcast[ :equity_with_loan ] * treshold ? self : nil end # # Auto Adjust implements a simple algorithm to ensure that an order is accepted @@ -112,8 +133,8 @@ def auto_adjust 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? + 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 @@ -122,6 +143,6 @@ def auto_adjust class Order include OrderFlow end # class - Connection.current.activate_plugin 'process-orders' +# Connection.current.activate_plugin 'process-orders' end # module IB diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb index 02938b7..fd9e626 100644 --- a/plugins/ib/process-orders.rb +++ b/plugins/ib/process-orders.rb @@ -94,7 +94,7 @@ def initialize_order_handling def request_open_orders q = Queue.new - subscription = subscribe( :OpenOrderEnd ) { q.push(true) } # signal succsess + 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. diff --git a/plugins/ib/symbols/combo.rb b/plugins/ib/symbols/combo.rb index e72ccc3..45f170f 100644 --- a/plugins/ib/symbols/combo.rb +++ b/plugins/ib/symbols/combo.rb @@ -6,45 +6,33 @@ module Combo extend Symbols def self.contracts - + base = 4500 @contracts ||= { #super.merge( - stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: 5000, + stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: base, expiry: IB::Option.next_expiry, trading_class: 'OESX' ) , - stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: 5000, back: '2m' , + stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: base, back: '2m' , front: IB::Option.next_expiry, trading_class: 'OESX' ), - stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: 4900, + stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: base - 200, expiry: IB::Option.next_expiry), - front: 4500, back: 5300), - stoxx_vertical: IB::Vertical.build( from: IB::Symbols::Index.stoxx, sell: 4500, buy: 5000, right: :put, + front: base - 400, back: base), + stoxx_vertical: IB::Vertical.build( from: IB::Symbols::Index.stoxx, sell: base - 200, buy: base + 200, right: :put, expiry: IB::Option.next_expiry, trading_class: 'OESX'), - zn_calendar: IB::Calendar.fabricate( IB::Symbols::Futures.zn.next_expiry, '3m') , + 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' - ), + [ 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' - ), - wti_coil: Bag.new( symbol: 'WTI', currency: 'USD', exchange: 'SMART', combo_legs: - [ ComboLeg.new( con_id: 55928698, action: :buy, exchange: 'IPE', ratio: 1), # WTI future June 2017 - ComboLeg.new( con_id: 55850663, action: :sell, exchange: 'IPE', ratio: 1 ) ], # COIL future June 2017 - description: 'Smart Future Spread WTI - COIL (June 2017) ' - ), - wti_brent: Bag.new( symbol: 'CL.BZ', currency: 'USD', exchange: 'NYMEX', combo_legs: - [ ComboLeg.new( con_id: 47207310, action: :buy, exchange: 'NYMEX', ratio: 1), # CL Dec'16 @NYMEX - ComboLeg.new( con_id: 47195961, action: :sell, exchange: 'NYMEX', ratio: 1 ) ], #BZ Dec'16 @NYMEX - description: ' WTI - Brent Spread (Dez. 2016)' - ) + 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 diff --git a/plugins/ib/symbols/futures.rb b/plugins/ib/symbols/futures.rb index 2adea11..810d908 100644 --- a/plugins/ib/symbols/futures.rb +++ b/plugins/ib/symbols/futures.rb @@ -19,37 +19,37 @@ def self.contracts :exchange => "CME", :currency => "USD", :multiplier => 20, - :description => "E-Mini Nasdaq 100 future"), + :description => "Mini Nasdaq 100 future"), :micro_nq => IB::Future.new(:symbol => "MNQ", :expiry => IB::Future.next_expiry, :exchange => "CME", :currency => "USD", :multiplier => 2, - :description => "E-Mini Nasdaq 100 future"), + :description => "Micro Nasdaq 100 future"), :es => IB::Future.new(:symbol => "ES", :expiry => IB::Future.next_expiry, :exchange => "CME", :currency => "USD", :multiplier => 50, - :description => "E-Mini S&P 500 future"), + :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 E-Mini S&P 500 future"), + :description => "Micro S&P 500 future"), :russell => IB::Future.new(:symbol => "RTY", :expiry => IB::Future.next_expiry, :exchange => "CME", :currency => "USD", :multiplier => 5, - :description => "Micro E-Mini Russell 2000 future"), + :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 E-Mini Russell 2000 future"), + :description => "Micro Russell 2000 future"), :zn => IB::Future.new( symbol: 'ZN', expiry: IB::Future.next_expiry, currency: 'USD', @@ -62,6 +62,11 @@ def self.contracts 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', @@ -81,7 +86,12 @@ def self.contracts expiry: IB::Future.next_expiry, currency: 'EUR', multiplier: 1, - description: 'Mini EuroStoxx 50 -Future'), + 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", diff --git a/plugins/ib/symbols/index.rb b/plugins/ib/symbols/index.rb index efac8f9..5362407 100644 --- a/plugins/ib/symbols/index.rb +++ b/plugins/ib/symbols/index.rb @@ -5,37 +5,37 @@ module Index extend Symbols def self.contracts - @contracts.presence || super.merge( - :dax => IB::Index.new(:symbol => "DAX", :currency => "EUR", exchange: 'EUREX', + @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" ), - :hsi => IB::Index.new( :symbol => 'HSI', :currency => 'HKD', exchange: 'HKFE', + :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', + :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', + :spx => IB::Index.new( :symbol => "SPX", :currency => "USD", exchange: 'CBOE', :description => "S&P 500 Stock Index"), - :vhsi => IB::Index.new( symbol: 'VHSI', exchange: 'HKFE', + :vhsi => IB::Index.new( symbol: 'VHSI', exchange: 'HKFE', :description => "Hang Seng Volatility Index"), - :vasx => IB::Index.new( symbol: 'XVI', exchange: 'ASX', + :vasx => IB::Index.new( symbol: 'XVI', exchange: 'ASX', :description => "ASX 200 Volatility Index") , - :vstoxx => IB::Index.new(:symbol => "V2TX", :currency => "EUR", exchange: 'EUREX', + :vstoxx => IB::Index.new( :symbol => "V2TX", :currency => "EUR", exchange: 'EUREX', :description => "VSTOXX Volatility Index"), - :vdax => IB::Index.new(:symbol => "VDAX", exchange: 'EUREX', + :vdax => IB::Index.new( :symbol => "VDAX", exchange: 'EUREX', :description => "German VDAX Volatility Index"), - :vix => IB::Index.new(:symbol => "VIX", exchange: 'CBOE', + :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") ) + :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 diff --git a/spec/ib/contracts/butterfly_spec.rb b/spec/ib/contracts/butterfly_spec.rb index 8617652..c515414 100644 --- a/spec/ib/contracts/butterfly_spec.rb +++ b/spec/ib/contracts/butterfly_spec.rb @@ -42,7 +42,8 @@ end context "create a limit-order" do - subject { IB::Limit.order contract: IB::Symbols::Combo.stoxx_butterfly, size: 1, price: 25 } + 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' 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/main_helper.rb b/spec/main_helper.rb index a9d97be..e3d03cd 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -43,7 +43,7 @@ def should_not_log *patterns def establish_connection *plugins ib = nil accounts = nil - if plugins.map( &:to_s ).include?("managed-accounts") || plugins.include?("process-orders") || plugins.include?('gateway') + if plugins.map( &:to_s ).then {|y| y.include?("managed-accounts") ||y.include?("process-orders") || y.include?('gateway')} OPTS[:connection].merge connect: false ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) ib.activate_plugin 'verify', 'process-orders', 'advanced-account' diff --git a/spec/order_helper.rb b/spec/order_helper.rb index 56a6f17..0ebc4fc 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -12,18 +12,18 @@ def place_the_order( contract: IB::Symbols::Stocks.wfc ) order = yield( get_contract_price( contract: contract) ) connection = IB::Connection.current - local_id = order.local_id || connection.next_local_id - - connection.send_message :PlaceOrder, :order => order, - :contract => if contract.con_id.to_i > 0 - Contract.new con_id: contract.con_id, - exchange: the_contract.exchange - else - contract - end - :local_id => local_id - IB::Connection.current.wait_for :OpenOrder, 3 - local_id # return value + + 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 @@ -58,7 +58,7 @@ def remove_open_orders 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 ! @@ -74,7 +74,7 @@ def remove_open_orders expect( o.local_id ).to be_an Integer expect( o.perm_id ).to be_an Integer expect(IB::VALUES[:clearing_intent].values). to include o.clearing_intent - expect( o.order_type ).to eq :limit + expect( o.order_type ).to eq( :limit ).or eq( :market ) expect( IB::VALUES[:tif].values ).to include o.tif #expect( o.status ).to match /Submit/ expect( o.clearing_intent ).to eq :ib @@ -106,7 +106,9 @@ def remove_open_orders expect( subject.serialize_pegged_order_fields).to be_empty expect( subject.serialize_mifid_order_fields.flatten.compact).to be_empty expect( subject.serialize_peg_best_and_mid).to be_empty - expect( subject.serialize_combo_legs).to be_empty + unless subject.contract.is_a? IB::Bag + expect( subject.serialize_combo_legs).to be_empty + end end # it end @@ -206,7 +208,6 @@ def remove_open_orders 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 @@ -229,17 +230,13 @@ def remove_open_orders # 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 + 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 + its( :realized_pnl ){is_expected.to be_a( BigDecimal ).or be_nil} end From 617775990b4b76a8a2bc60154307b5e9c7b913b1 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 24 Oct 2024 15:16:54 +0200 Subject: [PATCH 60/76] Added BigDecimal/util to provide 'to_d' method to numeric and string objects --- lib/ib-api.rb | 1 + lib/ib/support.rb | 2 +- plugins/ib/probability-of-expiring.rb | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/ib-api.rb b/lib/ib-api.rb index a523a4c..da451b4 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -3,6 +3,7 @@ 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' diff --git a/lib/ib/support.rb b/lib/ib/support.rb index 1f8c04b..f289ff6 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -29,7 +29,7 @@ def read_float end def read_decimal i= self.shift rescue nil - i = BigDecimal(i) unless i.blank? + 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 diff --git a/plugins/ib/probability-of-expiring.rb b/plugins/ib/probability-of-expiring.rb index e90c8d9..f36717a 100644 --- a/plugins/ib/probability-of-expiring.rb +++ b/plugins/ib/probability-of-expiring.rb @@ -84,7 +84,7 @@ def calculate_probability_of_expiring price: nil, error "ProbabilityOfExpiringCone needs strike as input" if strike.to_i.zero? if expiry.nil? - if last_trading_day == '' + if !last_trading_day.present? || last_trading_day.empty? error "ProbabilityOfExpiringCone needs expiry as input" else expiry = last_trading_day From bd67406d7a0c61ae621465bf6056b976d6c17b84 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 25 Oct 2024 21:09:07 +0200 Subject: [PATCH 61/76] Option greeks: contract.greek.greeks? is satisfied, if delta, gamma and vega are present --- bin/console.yml | 2 +- models/ib/option_detail.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/console.yml b/bin/console.yml index 8e0eda6..ac9d8dd 100644 --- a/bin/console.yml +++ b/bin/console.yml @@ -1,3 +1,3 @@ :gateway: "localhost:4002" -:tws: "10.247.8.109:7496" +:tws: "tws:7496" :client_id: 2000 diff --git a/models/ib/option_detail.rb b/models/ib/option_detail.rb index cc02c2f..67df94b 100644 --- a/models/ib/option_detail.rb +++ b/models/ib/option_detail.rb @@ -1,7 +1,6 @@ module IB - # Additional Option properties and Option-Calculations - class OptionDetail < IB::Base + class OptionDetail < Base include BaseProperties prop :delta, :gamma, :vega, :theta, # greeks @@ -31,14 +30,15 @@ def complete? 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 + # true if prices are received def prices? fields = [:implied_volatility, :under_price, :option_price] !fields.detect{|y| self.send(y).nil?} From f44a3951fc1d5c4b5dfdeca9e0aa2e5822a974e1 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 28 Oct 2024 08:41:45 +0100 Subject: [PATCH 62/76] Connection.reconnect --- lib/ib/connection.rb | 49 ++++++++++++++++++++++++++--------- lib/ib/plugins.rb | 5 ++-- lib/ib/prepare_data.rb | 4 +-- plugins/ib/symbols/options.rb | 1 + 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 68dfba2..c428e1d 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -50,17 +50,18 @@ def workflow_state event :initialize_managed_accounts, transitions_to: :account_based_operations end state :ready do - event :initialize_managed_accounts, transitions_to: :account_based_operations + 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 + 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 + event :initialize_order_handling, transitions_to: :account_based_orderflow # plugin process-orders end state :account_based_orderflow do @@ -72,6 +73,26 @@ def workflow_state end end + def reconnect + return if workflow_state == "virgin" + old_workflowstate = workflow_state.dup + disconnect! + + unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq + puts "subscrbers: #{@subscribers.inspect}" + + puts "ows: #{old_workflowstate}" + if ["ready", "lean_mode"].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 + def initialize host: '127.0.0.1:4002', # combination of host + port port: nil, @@ -108,18 +129,11 @@ def initialize host: '127.0.0.1:4002', # combination of host + port @connected = false @plugins.each do |name| - puts "activating #{name}" activate_plugin name end @next_local_id = nil - # TWS always sends NextValidId message at connect -subscribe save this id - self.subscribe(:NextValidId) do |msg| - self.logger.progname = "Connection" - @next_local_id = msg.local_id - self.logger.info { "Got next valid order id: #{@next_local_id}." } - end # # this block is executed before tws-communication is established # Its intended for globally available subscriptions of tws-messages @@ -161,6 +175,12 @@ def try_connection 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 self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible socket.initialising_handshake @@ -254,7 +274,6 @@ def unsubscribe *ids end end ### Working with received messages Hash - # Clear received messages Hash def clear_received *message_types @receive_lock.synchronize do @@ -266,6 +285,8 @@ 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| @@ -452,6 +473,7 @@ def subscribers 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 | # puts "THE deCODED MESSAGE #{ the_decoded_message.inspect}" msg_id = the_decoded_message.shift.to_i @@ -462,7 +484,10 @@ def process_message # 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] + + ## raising IB::TransmissionError if something went wrong. + ## the calling program has to initiate reconnection + 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? msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message) diff --git a/lib/ib/plugins.rb b/lib/ib/plugins.rb index 9f7870a..6d70ae0 100644 --- a/lib/ib/plugins.rb +++ b/lib/ib/plugins.rb @@ -1,13 +1,12 @@ module IB module Plugins def activate_plugin *names - root= Pathname.new( File.expand_path("../../../", __FILE__ )) - + 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 + "plugins/ib/#{n}.rb" + filename = root.join( "plugins", "ib", n+".rb" ) if filename.exist? if require filename @plugins << n diff --git a/lib/ib/prepare_data.rb b/lib/ib/prepare_data.rb index 735e583..7b2c797 100644 --- a/lib/ib/prepare_data.rb +++ b/lib/ib/prepare_data.rb @@ -35,8 +35,8 @@ def prepare_message data # # 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 + # + # 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? diff --git a/plugins/ib/symbols/options.rb b/plugins/ib/symbols/options.rb index c10d44e..4158ab0 100644 --- a/plugins/ib/symbols/options.rb +++ b/plugins/ib/symbols/options.rb @@ -49,6 +49,7 @@ def self.contracts :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, From b64ae0a84fbfaba0f72b882066ea58e7722bcecf Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 5 Nov 2024 17:26:18 +0100 Subject: [PATCH 63/76] Fixed account.preview(order) and order.check_margin(treshold) --- lib/ib/connection.rb | 48 ++++++++++++++++++---------------- plugins/ib/advanced-account.rb | 5 ++-- plugins/ib/order-flow.rb | 23 +++++++++++----- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index c428e1d..c2f8478 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -73,26 +73,6 @@ def workflow_state end end - def reconnect - return if workflow_state == "virgin" - old_workflowstate = workflow_state.dup - disconnect! - - unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq - puts "subscrbers: #{@subscribers.inspect}" - - puts "ows: #{old_workflowstate}" - if ["ready", "lean_mode"].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 - def initialize host: '127.0.0.1:4002', # combination of host + port port: nil, @@ -205,6 +185,11 @@ def try_connection "#{@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 @@ -220,6 +205,26 @@ def disconnect end + def reconnect + return if workflow_state == "virgin" + old_workflowstate = workflow_state.dup + disconnect! + + unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq + puts "subscrbers: #{@subscribers.inspect}" + + puts "ows: #{old_workflowstate}" + if ["ready", "lean_mode"].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 @@ -398,8 +403,7 @@ def send_message what, *args end rescue Errno::EPIPE logger.error{ "Broken Pipe, trying to reconnect" } - disconnect! - try_connection! + reconnect retry end ## return the transmitted message diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index 9f7d9d2..c39ca00 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -270,7 +270,8 @@ def modify_order local_id: nil, order_ref: nil, order:nil, contract: nil # # Submits a "WhatIf" Order # - # Returns the presubmiited order record, where the local_id is erased and the what_if-flag is false + # 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 @@ -303,7 +304,7 @@ def preview order:, contract: nil, **args_which_are_ignored 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.init_margin.nil? + 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 diff --git a/plugins/ib/order-flow.rb b/plugins/ib/order-flow.rb index 0dc793b..9569f93 100644 --- a/plugins/ib/order-flow.rb +++ b/plugins/ib/order-flow.rb @@ -82,20 +82,29 @@ def modify # 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 + # 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.forecast[ :init_margin ].nil? - ib = Connection.current -# client = ib.clients.find{|y| y.account == account} -# net_liquidation = client.account_data_scan( /NetLiquidation$/ ).first.value.to_i - buffer = order_state.forcast.then{ |x| x[ :equity_with_loan ] - x[ :init_margin ] } - buffer > order_state.forcast[ :equity_with_loan ] * treshold ? self : nil + 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 From 4b9d79c4cdbb55115e7d03856dd6c5937a2fb31a Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sun, 17 Nov 2024 20:03:51 +0100 Subject: [PATCH 64/76] Mark all Workflow-State-Methods as `protected --- bin/console | 6 ++ lib/ib/connection.rb | 41 +++++------ plugins/ib/connection-tools.rb | 39 +++-------- plugins/ib/managed-accounts.rb | 122 ++++++++++++++++++--------------- plugins/ib/order-flow.rb | 2 +- plugins/ib/process-orders.rb | 3 +- spec/ib/connect_spec.rb | 62 ++++++++++------- spec/ib/connection_spec.rb | 2 +- spec/ib/stock_spec.rb | 4 +- spec/main_helper.rb | 1 - 10 files changed, 144 insertions(+), 138 deletions(-) diff --git a/bin/console b/bin/console index 2f8f9b4..ad3f8a2 100755 --- a/bin/console +++ b/bin/console @@ -31,6 +31,11 @@ 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 @@ -69,6 +74,7 @@ end # Array "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 diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index c2f8478..a236127 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -119,10 +119,6 @@ def initialize host: '127.0.0.1:4002', # combination of host + port # Its intended for globally available subscriptions of tws-messages yield self if block_given? -# if connect -# Kernel.exit if @next_local_id.nil? # emergency exit. - # update_next_order_id should have raised an error -# end end # read actual order_id and @@ -149,6 +145,7 @@ def connected? end # ### Event – call through Connection-object.try_connection! + protected def try_connection logger.progname='IB::Connection#Event:TryConnection' if connected? @@ -205,16 +202,22 @@ def disconnect end + 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 def reconnect return if workflow_state == "virgin" old_workflowstate = workflow_state.dup - disconnect! + disconnect! unless disconnected? unsubscribe *@subscribers.map{|_,m| m.keys}.flatten.uniq - puts "subscrbers: #{@subscribers.inspect}" - puts "ows: #{old_workflowstate}" - if ["ready", "lean_mode"].include? old_workflowstate + if ["ready", "lean_mode", "disconnected"].include? old_workflowstate try_connection! else activate_managed_accounts! @@ -236,7 +239,7 @@ 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 = @@ -278,6 +281,8 @@ def unsubscribe *ids end.flatten end end + + ### Working with received messages Hash # Clear received messages Hash def clear_received *message_types @@ -338,6 +343,7 @@ def wait_for *args, &block ### Working with Incoming messages from IB + protected def reader_running? @reader_running && @reader_thread && @reader_thread.alive? end @@ -384,6 +390,7 @@ def process_messages poll_time = 50 # in msec # Send an outgoing message. # returns the used request_id if appropiate, otherwise "true" + public def send_message what, *args message = case @@ -417,7 +424,7 @@ def send_message what, *args 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 "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 @next_local_id += 1 @@ -447,6 +454,7 @@ 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 @@ -464,7 +472,6 @@ def start_reader 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 @@ -531,17 +538,5 @@ def satisfied? *conditions end end end -# private - # 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 end # class Connection end # module IB diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 85ca762..38b1968 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -24,9 +24,11 @@ module ConnectionTools # # check_connection reconnects if necessary and returns false if the connection is lost. # - # It delays the process by 6 ms (150 MBit Cable connection, loc. Europe) + # Individial subscriptions have to be placed **after** checking the connection! # - # a = Time.now; G.check_connection; b= Time.now ;b-a + # 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 @@ -37,7 +39,7 @@ def check_connection loop do begin send_message(:RequestCurrentTime) # 10 ms ## - th = Thread.new{ sleep 1 ; q.push nil } + th = Thread.new{ sleep 0.1 ; q.push nil } result = q.pop count+=1 break if result || count > 10 @@ -45,12 +47,13 @@ def check_connection count +=1 retry rescue IB::Error # not connected - disconnect! logger.info{"not connected ... trying to reconnect "} - sleep 0.1 - try_connection! + 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"} end end unsubscribe z @@ -64,6 +67,7 @@ def check_connection # Unsuccessful connecting attemps are logged. # # + protected def try_connection maximal_count_of_retry=100 i= -1 @@ -95,7 +99,6 @@ def try_connection maximal_count_of_retry=100 self # return connection end # def - private def submit_to_alert_1102 current.subscribe( :Alert ) do if [2102, 1101].include? msg.id.to_i # Connectivity between IB and Trader Workstation @@ -107,33 +110,11 @@ def submit_to_alert_1102 end end - - end - - module ReConnect - def safe_reconnect - used_plugins = current.plugins - used_client_id = current.client_id - used_host = current.host - used_port = current.port - used_received = if current.received.nil? || current.received.empty? - false - else - true - end - current &.disconnect - current = nil - c = Connection.new client_id: used_client_id, host: used_host, port: used_port - - - end - end class Connection alias _try_connection try_connection include ConnectionTools - #extend ReConnect end diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed-accounts.rb index 65344a7..1614585 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed-accounts.rb @@ -30,12 +30,10 @@ module IB Standard usage - ib = Connection.new connect: false do | c | - c.activate_plugin 'managed-accounts' - c.initialize_managed_accounts # connects to the tws - c.get_account_data # populates c.clients - end - + 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 @@ -43,45 +41,6 @@ module IB module ManagedAccounts -=begin ---------------------------- InitializeManageAccounts ---------------------------------- - -If initiated with the parameter `force: true`, a reconnect is performed to initiate the -transmission of available managed-accounts. - -=end - def initialize_managed_accounts( force: false ) - queue = Queue.new - # 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 } - @accounts = [] - - if connected? - disconnect! - sleep(0.1) - end - try_connection! - result = queue.pop - unsubscribe man_id, rec_id, error_id - - @accounts - - end # def =begin clients returns a list of Account-Objects @@ -180,7 +139,47 @@ def all_contracts end - private +=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 @@ -196,7 +195,17 @@ def all_contracts # 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 @@ -206,9 +215,6 @@ def subscribe_account_updates continuously: true IB::Connection.logger.debug { "#{account.account} :: #{msg.account_value.to_human }"} when IB::Messages::Incoming::AccountDownloadEnd if account.account_values.size > 10 - # simply don't cancel the subscription if continuously is specified - # the connected flag is set in any case, indicating that valid data are present - # send_message :RequestAccountData, subscribe: false, account_code: account.account unless continuously 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 @@ -216,17 +222,23 @@ def subscribe_account_updates continuously: true end when IB::Messages::Incoming::PortfolioValue account.contracts << msg.contract unless account.contracts.detect{|y| y.con_id == msg.contract.con_id } - account.portfolio_values << msg.portfolio_value -# msg.portfolio_value.account = account -# # link contract -> portfolio value -# account.contracts.find{ |x| x.con_id == msg.contract.con_id } -# .portfolio_values -# .update_or_create( msg.portfolio_value ) { :account } + 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 diff --git a/plugins/ib/order-flow.rb b/plugins/ib/order-flow.rb index 9569f93..3d598ac 100644 --- a/plugins/ib/order-flow.rb +++ b/plugins/ib/order-flow.rb @@ -102,7 +102,7 @@ def check_margin treshold = 0.1 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})" + 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 diff --git a/plugins/ib/process-orders.rb b/plugins/ib/process-orders.rb index fd9e626..2cbdd71 100644 --- a/plugins/ib/process-orders.rb +++ b/plugins/ib/process-orders.rb @@ -19,6 +19,7 @@ module IB =end module ProcessOrders + protected def initialize_order_handling subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg| @@ -90,7 +91,7 @@ def initialize_order_handling # # Waits until the OpenOrderEnd-Message is received - + public def request_open_orders q = Queue.new diff --git a/spec/ib/connect_spec.rb b/spec/ib/connect_spec.rb index 3c89e72..ddf7bea 100644 --- a/spec/ib/connect_spec.rb +++ b/spec/ib/connect_spec.rb @@ -1,4 +1,5 @@ require "main_helper" +require 'rspec/given' describe "Connect to Gateway or TWS" do before(:all){ establish_connection } @@ -6,30 +7,44 @@ after(:all) { close_connection } context "A new connection" do - it{ expect( IB::Connection.current ).to be_a IB::Connection } + Given( :connection ){ IB::Connection.current } + Then { connection.is_a? IB::Connection } + end - it "has the proper state" do - expect( IB::Connection.current.ready? ).to be_truthy - expect( IB::Connection.current.workflow_state ).to eq 'ready' - 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 + context "Workflow States " do + context "ready" do + Given( :connection ){ IB::Connection.current } - it "clients are NOT present" do - expect{ IB::Connection.current.clients }.to raise_error NoMethodError + Then { connection.ready? } + Then { connection.workflow_state == 'ready' } end - it "can be disconnected" do + + context "disconnected" do +# Given( :connection ){ IB::Connection.current } + it "initiate disconnect" do + + ib = IB::Connection.current - expect( ib.ready? ).to be_truthy 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 - - context " load plugins in the fly" do + + +# 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 @@ -44,21 +59,18 @@ end - - it "if disconnected, account-based operations can be loaded" do + it "state `account-based operations` can be loaded through managed-accounts plugin" do ib = IB::Connection.current expect( ib.workflow_state ).to eq 'ready' - expect { ib.activate_plugin :managed_accounts } .to raise_error Workflow::NoTransitionAllowed - expect( ib.ready? ).to be_truthy - expect( ib.plugins ).not_to include "managed-accounts" - expect { ib.disconnect! }.to change{ ib.workflow_state }.to 'disconnected' - expect { ib.activate_plugin :managed_accounts } .not_to raise_error + 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_an Array - + 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 index 3d9b7bc..791b323 100644 --- a/spec/ib/connection_spec.rb +++ b/spec/ib/connection_spec.rb @@ -24,7 +24,7 @@ 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', connect: false + 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 diff --git a/spec/ib/stock_spec.rb b/spec/ib/stock_spec.rb index ddd74ce..0da2456 100644 --- a/spec/ib/stock_spec.rb +++ b/spec/ib/stock_spec.rb @@ -19,7 +19,7 @@ Then { ms_stock == msft } end - describe "specify the symbil as string " do + 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 } @@ -31,7 +31,7 @@ Given( :msft ) { IB::Symbols::Stocks.msft } When( :verified_microsoft ){ msft.verify.first } - Then{ msft != verified_microsoft } + Then{ msft == verified_microsoft } Then{ verified_microsoft.con_id == 272093 } Then{ verified_microsoft.contract_detail.is_a? IB::ContractDetail } diff --git a/spec/main_helper.rb b/spec/main_helper.rb index e3d03cd..2081a6a 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -44,7 +44,6 @@ 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')} - OPTS[:connection].merge connect: false ib = IB::Connection.new **OPTS[:connection].merge(:logger => mock_logger) ib.activate_plugin 'verify', 'process-orders', 'advanced-account' ib.get_account_data From cf40645b2cb0a3d8b5767664bdbf13767e06a83d Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 19 Nov 2024 12:36:35 +0100 Subject: [PATCH 65/76] Reimplementing RawMessageParser (Brandon Coleman) --- lib/ib/connection.rb | 22 ++++- lib/ib/raw_message_parser.rb | 98 +++++++++++++++++++ spec/ib/raw_message_spec.rb | 179 +++++++++++++++++++++++++++++++++++ spec/main_helper.rb | 12 +-- 4 files changed, 300 insertions(+), 11 deletions(-) create mode 100644 lib/ib/raw_message_parser.rb create mode 100644 spec/ib/raw_message_spec.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index a236127..145334f 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -30,9 +30,9 @@ class Connection alias next_order_id next_local_id alias next_order_id= next_local_id= - def workflow_state - @workflow_state - end + def workflow_state + @workflow_state + end workflow do state :virgin do @@ -106,6 +106,8 @@ def initialize host: '127.0.0.1:4002', # combination of host + port @subscribe_lock = Mutex.new @receive_lock = Mutex.new @message_lock = Mutex.new + + @parser = nil @connected = false @plugins.each do |name| @@ -161,7 +163,9 @@ def try_connection self.socket = IB::Socket.open(@host, @port) # raises Errno::ECONNREFUSED if no connection is possible socket.initialising_handshake - socket.decode_message( socket.receive_messages ) do | the_message | + @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 @@ -169,6 +173,7 @@ def try_connection @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 @@ -485,7 +490,8 @@ 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 | +# 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 @@ -498,9 +504,15 @@ def process_message ## 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. diff --git a/lib/ib/raw_message_parser.rb b/lib/ib/raw_message_parser.rb new file mode 100644 index 0000000..904fdbd --- /dev/null +++ b/lib/ib/raw_message_parser.rb @@ -0,0 +1,98 @@ +# 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) + return true if length <= 5000 + + raise 'Message is longer than sane max length' + 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/main_helper.rb b/spec/main_helper.rb index 2081a6a..cdecf74 100644 --- a/spec/main_helper.rb +++ b/spec/main_helper.rb @@ -44,12 +44,12 @@ 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}" + 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) From b5907f59d9692f96f42cb89c9077e4d4d66b4c27 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 21 Nov 2024 10:44:23 +0100 Subject: [PATCH 66/76] Updates/Fixes to Spread-Prototype StockSpread --- lib/ib/prepare_data.rb | 12 ++--- plugins/ib/spread-prototypes/calendar.rb | 36 +++++++------- plugins/ib/spread-prototypes/stock-spread.rb | 21 +++++--- spec/ib/contracts/calendar_spec.rb | 2 +- spec/ib/contracts/verify_spec.rb | 50 ++++++++++++++++++++ 5 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 spec/ib/contracts/verify_spec.rb diff --git a/lib/ib/prepare_data.rb b/lib/ib/prepare_data.rb index 7b2c797..193f60c 100644 --- a/lib/ib/prepare_data.rb +++ b/lib/ib/prepare_data.rb @@ -41,18 +41,18 @@ 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] + 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") + message = msg.unpack( "A#{size}" ).first.split( "\0" ) # DEBUG display raw decoded message on STDOUT -# STDOUT::puts "message: #{message}" + # STDOUT::puts "message: #{message}" if block_given? yield message else - m[message.shift.to_i] = message + m[ message.shift.to_i ] = message end - msg = msg[size..-1] + msg = msg[ size .. -1 ] end return m unless m == {} end diff --git a/plugins/ib/spread-prototypes/calendar.rb b/plugins/ib/spread-prototypes/calendar.rb index 57cb40c..78971a0 100644 --- a/plugins/ib/spread-prototypes/calendar.rb +++ b/plugins/ib/spread-prototypes/calendar.rb @@ -42,46 +42,44 @@ def fabricate master, the_other_expiry # Call with # IB::Calendar.build from: IB::Contract, front: an_expiry, back: an_expiry, # right: {put or call}, strike: a_strike - def build from:, **fields + def build from:, front: nil, back: nil, right: :put, strike: nil, **fields underlying = if from.is_a? IB::Option - fields[:right] = from.right unless fields.key?(:right) - fields[:front] = from.expiry unless fields.key(:front) - fields[:strike] = from.strike unless fields.key?(:strike) - 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? + 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 - kind = { :front => fields.delete(:front), :back => fields.delete(:back) } - error "Specification of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil) + 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.attributes - .slice( :currency, :symbol, :exchange) + leg_prototype = IB::Option.new underlying.invariant_attributes.except( :sec_type ) + .slice( :currency, :symbol, :exchange ) .merge(defaults) .merge( fields ) - kind[:back] = IB::Spread.transform_distance kind[:front], kind[:back] + .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 + 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 - + 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 ) + the_spread.description = the_description( the_spread ) rescue nil end end def defaults - super.merge expiry: IB::Future.next_expiry, - right: :put + super.merge right: :put +# expiry: IB::Future.next_expiry, end diff --git a/plugins/ib/spread-prototypes/stock-spread.rb b/plugins/ib/spread-prototypes/stock-spread.rb index baf5bdc..d39e386 100644 --- a/plugins/ib/spread-prototypes/stock-spread.rb +++ b/plugins/ib/spread-prototypes/stock-spread.rb @@ -18,16 +18,23 @@ class << self def fabricate *underlying, ratio: [1,-1], **args # are_stocks = ->(l){ l.all?{|y| y.is_a? IB::Stock} } - legs = underlying.map{|y| y.is_a?( IB::Stock ) ? y.merge(**args) : IB::Stock.new( symbol: y ).merge(**args)} + 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 - + 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 end end diff --git a/spec/ib/contracts/calendar_spec.rb b/spec/ib/contracts/calendar_spec.rb index c698ad7..42caa3d 100644 --- a/spec/ib/contracts/calendar_spec.rb +++ b/spec/ib/contracts/calendar_spec.rb @@ -30,7 +30,7 @@ right: :put, trading_class: 'OESX', front: IB::Option.next_expiry , - back: '-1m' + back: '3m' ) } it{ puts subject.as_table } 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 From 70c9bb85ea6015b895d78840ac60d3a797753f85 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 21 Nov 2024 18:47:08 +0100 Subject: [PATCH 67/76] Adapted Spread-Prototypes to V-10, incl. Specs --- plugins/ib/spread-prototypes/stock-spread.rb | 2 + plugins/ib/spread-prototypes/straddle.rb | 12 ++-- plugins/ib/spread-prototypes/strangle.rb | 26 ++++--- plugins/ib/symbols/combo.rb | 16 +++-- spec/combo_helper.rb | 10 ++- spec/ib/contracts/spread_spec.rb | 6 +- spec/ib/contracts/stock_spread_spec.rb | 75 ++++++++++++++++++++ spec/ib/contracts/straddle_spec.rb | 57 +++++++++++++++ spec/ib/contracts/strangle_spec.rb | 61 ++++++++++++++++ spec/ib/contracts/vertical_spec.rb | 56 +++++++++++++++ 10 files changed, 290 insertions(+), 31 deletions(-) create mode 100644 spec/ib/contracts/stock_spread_spec.rb create mode 100644 spec/ib/contracts/straddle_spec.rb create mode 100644 spec/ib/contracts/strangle_spec.rb create mode 100644 spec/ib/contracts/vertical_spec.rb diff --git a/plugins/ib/spread-prototypes/stock-spread.rb b/plugins/ib/spread-prototypes/stock-spread.rb index d39e386..d27f841 100644 --- a/plugins/ib/spread-prototypes/stock-spread.rb +++ b/plugins/ib/spread-prototypes/stock-spread.rb @@ -15,6 +15,8 @@ class << self # 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} } diff --git a/plugins/ib/spread-prototypes/straddle.rb b/plugins/ib/spread-prototypes/straddle.rb index ef2b083..a3527e6 100644 --- a/plugins/ib/spread-prototypes/straddle.rb +++ b/plugins/ib/spread-prototypes/straddle.rb @@ -36,11 +36,11 @@ def build from:, ** fields fabricate from.merge **fields else initialize_spread( from ) do | the_spread | - leg_prototype = IB::Option.new from.invariant_attributes - .slice( :currency, :symbol, :exchange) - .merge(defaults) - .merge( fields ) - puts leg_prototype.attributes + 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 @@ -52,7 +52,7 @@ def build from:, ** fields end def defaults - super.merge expiry: IB::Future.next_expiry + super.merge expiry: IB::Option.next_expiry end def requirements diff --git a/plugins/ib/spread-prototypes/strangle.rb b/plugins/ib/spread-prototypes/strangle.rb index b7397b3..93d0d4c 100644 --- a/plugins/ib/spread-prototypes/strangle.rb +++ b/plugins/ib/spread-prototypes/strangle.rb @@ -20,15 +20,16 @@ def fabricate master, distance initialize_spread( master ) do | the_spread | + the_spread.add_leg master.verify.first the_spread.add_leg master - 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 ) ) + con_id: 0 ) + .verify.first error "Initialisation of Legs failed" if the_spread.legs.size != 2 - the_spread.description = the_description( the_spread ) +# the_spread.description = the_description( the_spread ) end end @@ -41,28 +42,25 @@ def fabricate master, distance # # Call with # IB::Strangle.build from: IB::Contract, p: a_value, c: a_value, expiry: yyyymm(dd) - def build from:, **fields + def build from:, p: nil, c: nil, expiry: nil, **fields underlying = if from.is_a? IB::Option - fields[:p] = from.strike unless fields.key?(:p) || from.right == :call - fields[:c] = from.strike unless fields.key?(:c) || from.right == :puta - 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? + p ||= from.strike + c ||= from.strike + expiry ||= from.expiry details = from.verify.first.contract_detail - IB::Contract.new( con_id: details.under_con_id, + IB::Contract.new( con_id: details.under_con_id, currency: from.currency, - exchange: from.exchange) - .verify.first - .essential + exchange: from.exchange) .verify.first .essential else from end - kind = { :p => fields.delete(:p), :c => fields.delete(:c) } + 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) diff --git a/plugins/ib/symbols/combo.rb b/plugins/ib/symbols/combo.rb index 45f170f..eea3b57 100644 --- a/plugins/ib/symbols/combo.rb +++ b/plugins/ib/symbols/combo.rb @@ -6,17 +6,21 @@ module Combo extend Symbols def self.contracts - base = 4500 + base = 4800 + exp = IB::Option.next_expiry @contracts ||= { #super.merge( stoxx_straddle: IB::Straddle.build( from: IB::Symbols::Index.stoxx, strike: base, - expiry: IB::Option.next_expiry, trading_class: 'OESX' ) , + expiry: exp, trading_class: 'OESX' ) , stoxx_calendar: IB::Calendar.build( from: IB::Symbols::Index.stoxx, strike: base, back: '2m' , - front: IB::Option.next_expiry, trading_class: 'OESX' ), + front: exp, trading_class: 'OESX' ), stoxx_butterfly: IB::Butterfly.fabricate( IB::Symbols::Options.stoxx.merge( strike: base - 200, - expiry: IB::Option.next_expiry), + 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: IB::Option.next_expiry, trading_class: 'OESX'), + 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: diff --git a/spec/combo_helper.rb b/spec/combo_helper.rb index 4a0271e..f7628e8 100644 --- a/spec/combo_helper.rb +++ b/spec/combo_helper.rb @@ -45,7 +45,7 @@ def atm_option stock RSpec.shared_examples 'a valid ES-FUT Combo' do - its( :exchange ) { should eq 'GLOBEX' } + its( :exchange ) { should eq 'CME' } its( :symbol ) { should eq "ES" } # its( :market_price ) { should be_a Numeric } end @@ -58,10 +58,16 @@ def atm_option stock RSpec.shared_examples 'a valid wfc-stock Combo' do - its( :exchange ) { should eq 'EDGX' } + 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 } diff --git a/spec/ib/contracts/spread_spec.rb b/spec/ib/contracts/spread_spec.rb index 6156cf6..bee0b30 100644 --- a/spec/ib/contracts/spread_spec.rb +++ b/spec/ib/contracts/spread_spec.rb @@ -13,8 +13,8 @@ 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.size ).to eq 5 - expect( subject.serialize_combo_legs.flatten.slice(1,8 )).to eq [ con_ids[0], + 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 ] @@ -64,7 +64,7 @@ it_behaves_like "serialize limit order fields" it_behaves_like "serialize two Combo-legs" - it { expect( subject.serialize_combo_legs ).to eq [ the_spread.serialize_legs, + it { expect( subject.serialize_combo_legs(the_spread) ).to eq [ the_spread.serialize_legs, 0 ,[], 0 , [] ] } # leg-prices + combo-params 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/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 From 453fdee48138b3a58149f7f51d8c4aa563365cb4 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Fri, 22 Nov 2024 20:57:01 +0100 Subject: [PATCH 68/76] Fixed using Spreads (IB::Bag) as contract in orders --- lib/ib/connection.rb | 19 ++ lib/ib/messages/outgoing/place_order.rb | 16 +- models/ib/bag.rb | 25 +-- models/ib/contract.rb | 2 +- models/ib/order.rb | 8 +- models/ib/spread.rb | 16 +- plugins/ib/advanced-account.rb | 4 +- plugins/ib/spread-prototypes.rb | 2 +- plugins/ib/spread-prototypes/stock-spread.rb | 2 +- spec/ib/messages/outgoing/market_data_spec.rb | 68 +++++++ spec/ib/messages/outgoing/place_order_spec.rb | 49 +++++ spec/ib/orders/combo_spec.rb | 69 +++++++ spec/ib/orders/order_flow_spec.rb | 111 +++++++++++ spec/ib/orders/placement_spec.rb | 175 ++++++++++++++++++ spec/ib/orders/trades_spec.rb | 122 ++++++++++++ spec/ib/plugins/auto_adjust_spec.rb | 4 +- .../order-prototypes/adaptive_order_spec.rb | 41 ---- .../discretionary_order_spec.rb | 57 ++---- .../order-prototypes/limit_order_spec.rb | 40 ---- spec/order_helper.rb | 2 +- 20 files changed, 654 insertions(+), 178 deletions(-) create mode 100644 spec/ib/messages/outgoing/market_data_spec.rb create mode 100644 spec/ib/messages/outgoing/place_order_spec.rb create mode 100644 spec/ib/orders/combo_spec.rb create mode 100644 spec/ib/orders/order_flow_spec.rb create mode 100644 spec/ib/orders/placement_spec.rb create mode 100644 spec/ib/orders/trades_spec.rb diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index 145334f..aeb9ec9 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -215,6 +215,25 @@ def disconnect # 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 diff --git a/lib/ib/messages/outgoing/place_order.rb b/lib/ib/messages/outgoing/place_order.rb index b698036..9d99799 100644 --- a/lib/ib/messages/outgoing/place_order.rb +++ b/lib/ib/messages/outgoing/place_order.rb @@ -22,9 +22,9 @@ def encode order.serialize_auxilery_order_fields # including advisory order fields ] - if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] # 103 +# if server_version >= KNOWN_SERVERS[:min_server_ver_models_support] # 103 fields.push(order.model_code ) - end + # end fields += [ order[:short_sale_slot] , # 0 only for retail, 1 or 2 for institution (Institutional) @@ -89,19 +89,19 @@ def encode order.adjustable_trailing_unit ] - fields.push(order.ext_operator) if server_version >= KNOWN_SERVERS[:min_server_ver_ext_operator] # 105 + 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.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 +# 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 + 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) diff --git a/models/ib/bag.rb b/models/ib/bag.rb index 7513b20..8cf0704 100644 --- a/models/ib/bag.rb +++ b/models/ib/bag.rb @@ -9,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" @@ -31,8 +34,6 @@ def con_id= arg ### 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(',') @@ -44,26 +45,6 @@ def same_legs? other legs_description.split(',').sort == other.legs_description.split(',').sort end -# def serialize_legs dest = :order -# unless dest == :order -# super -# else -# [ combo_legs.size, -# combo_legs.map do |the_leg| -# [ -# the_leg.con_id, -# the_leg.ratio, -# the_leg.side.to_sup, -# the_leg.exchange, -# the_leg[:open_close], -# the_leg[:short_sale_slot], -# the_leg.designated_location, -# the_leg.exempt_code -# ] -# end -# ] -# end -# end # Contract comparison def == other diff --git a/models/ib/contract.rb b/models/ib/contract.rb index 9fd2a78..f14daa8 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -180,7 +180,7 @@ def serialize_legs *fields # :nodoc: when combo_legs.empty? [0] else - [combo_legs.size, combo_legs.map( &:serialize ) ] + [combo_legs.size, combo_legs.map{|x| x.serialize :extended} ] end end diff --git a/models/ib/order.rb b/models/ib/order.rb index f865860..77dbdde 100644 --- a/models/ib/order.rb +++ b/models/ib/order.rb @@ -444,10 +444,10 @@ def default_attributes # default valus are taken from order.java def serialize_combo_legs(contract) if contract.bag? [ contract.serialize_legs, - leg_prices.size, - leg_prices, - combo_params.size, - combo_params.to_a + # 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 [] diff --git a/models/ib/spread.rb b/models/ib/spread.rb index ad7f841..3a9cbed 100644 --- a/models/ib/spread.rb +++ b/models/ib/spread.rb @@ -131,14 +131,14 @@ def con_id end - def non_guaranteed= x - super.merge combo_params: [ ['NonGuaranteed', x] ] - end - - - def non_guaranteed - combo_params['NonGuaranteed'] - 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 diff --git a/plugins/ib/advanced-account.rb b/plugins/ib/advanced-account.rb index c39ca00..ec2abc7 100644 --- a/plugins/ib/advanced-account.rb +++ b/plugins/ib/advanced-account.rb @@ -152,7 +152,7 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true if auto_adjust wrong_order = nil the_local_id = -1 - ib.logger.warn "adjusting order-price" + 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 @@ -179,8 +179,6 @@ def place_order order:, contract: nil, auto_adjust: true, convert_size: true 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 - # apply non_guarenteed and other stuff bound to the contract to order. - order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank? # 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... diff --git a/plugins/ib/spread-prototypes.rb b/plugins/ib/spread-prototypes.rb index 417c888..3ecb40e 100644 --- a/plugins/ib/spread-prototypes.rb +++ b/plugins/ib/spread-prototypes.rb @@ -58,7 +58,7 @@ def parameters the_output = ->(var){ var.empty? ? "none" : var.map{|x| x.join(" --> ") }.join("\n\t: ")} "Required : " + the_output[requirements] + "\n --------------- \n" + - "Optional : " + the_output[optional] + "\n --------------- \n" + "Optional : " + the_output[optional] + "\n --------------- \n" end end diff --git a/plugins/ib/spread-prototypes/stock-spread.rb b/plugins/ib/spread-prototypes/stock-spread.rb index d27f841..b904094 100644 --- a/plugins/ib/spread-prototypes/stock-spread.rb +++ b/plugins/ib/spread-prototypes/stock-spread.rb @@ -37,6 +37,7 @@ def fabricate *underlying, ratio: [1,-1], **args 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 @@ -48,7 +49,6 @@ def the_description spread # always route a order as NonGuaranteed def order_requirements - { combo_params: ['NonGuaranteed', true] } end end # class 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/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 index e37579c..8ef995f 100644 --- a/spec/ib/plugins/auto_adjust_spec.rb +++ b/spec/ib/plugins/auto_adjust_spec.rb @@ -2,8 +2,8 @@ describe "Connect to TWS and activate Plugin" do before(:all) do - establish_connection - c = IB::Connection.current + establish_connection :gateway + c = IB::Connection.current c.activate_plugin "verify" c.activate_plugin "order-prototypes" c.activate_plugin "auto-adjust" diff --git a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb index 933623f..a6bc6ac 100644 --- a/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/adaptive_order_spec.rb @@ -34,45 +34,4 @@ end -# -# 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields -# if subject.server_version < 177 -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields -# else -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields -## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields -# end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields -# -# 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 index ec08276..30354eb 100644 --- a/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/discretionary_order_spec.rb @@ -7,6 +7,8 @@ ib = IB::Connection.current ib.activate_plugin 'order-prototypes' ib.activate_plugin 'symbols' + ib.activate_plugin 'process-orders' + ib.activate_plugin 'order-flow' end @@ -14,7 +16,7 @@ Given( :volatile_stock ){ IB::Stock.new symbol: 'TSLA' } Given( :size ){ 100 } - Given( :price){ 180 } # Public Limit price + Given( :price){ 380 } # Public Limit price Given( :secret ){ 5 } # Secret discount offered to the seller When( :order ){ IB::Discretionary.order size: size, price: price, @@ -43,55 +45,18 @@ end context "place example orders" do - Given( :account ){ IB::Connection.current.clients.detect{| i | i.account == ACCOUNT } } - When( :order_id ){ account.place order: order, contract: volatile_stock } - Then{ account.orders.size > 0 } - Then{ account.orders.last.contract == volatile_stock } + 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 -# -# 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields -# if subject.server_version < 177 -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields -# else -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields -## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields -# end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields -# -# 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 index febcc9b..02333cf 100644 --- a/spec/ib/plugins/order-prototypes/limit_order_spec.rb +++ b/spec/ib/plugins/order-prototypes/limit_order_spec.rb @@ -37,45 +37,5 @@ end -# -# 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 -## expect( subject.encode[5]). to eq ["",0,nil,nil] # auxilery order fields -# if subject.server_version < 177 -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil,nil]] # advisory order fields -# else -# expect( subject.encode[4]). to eq ["",0,nil,nil,[nil,nil,nil]] # advisory order fields -## expect( subject.encode[6]). to eq [nil,nil,nil] # advisory order fields -# end -## expect( subject.encode[7]). to eq ["",0,"",-1,0,nil,nil] # regulatory order fields -## expect( subject.encode[8]). to eq [false, "", "", false, false, false, 0, nil, "", "", "", "", false, ["", ""]] # algo order fields -1- -## expect( subject.encode[9]). to eq ["",""] # empty delta neutral order fields -## expect( subject.encode[10]). to eq [0,""] # empty delta neutral order fields -# -# end -# # end # describe IB::Messages:Outgoing diff --git a/spec/order_helper.rb b/spec/order_helper.rb index 0ebc4fc..dafea49 100644 --- a/spec/order_helper.rb +++ b/spec/order_helper.rb @@ -107,7 +107,7 @@ def remove_open_orders expect( subject.serialize_mifid_order_fields.flatten.compact).to be_empty expect( subject.serialize_peg_best_and_mid).to be_empty unless subject.contract.is_a? IB::Bag - expect( subject.serialize_combo_legs).to be_empty + expect( subject.serialize_combo_legs(subject.contract)).to be_empty end end # it From 15cea6cddee58cfc92193ee9ba494b5c73ed26cc Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Sun, 24 Nov 2024 07:41:53 +0100 Subject: [PATCH 69/76] Enabling usage of `as_table` for incompletely initialized contracts --- models/ib/contract.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/models/ib/contract.rb b/models/ib/contract.rb index f14daa8..c2ec17c 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -383,10 +383,10 @@ def table_row { 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}, + { 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 From dab5dc09a7ca93004f9eb1b873e50f0c638e1f9f Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Wed, 27 Nov 2024 09:59:38 +0100 Subject: [PATCH 70/76] Option#next_expiry raises IB::LoadError if no contract found in the specified month --- lib/ib/connection.rb | 4 ++-- models/ib/option.rb | 4 +++- plugins/ib/connection-tools.rb | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index aeb9ec9..78b13e1 100644 --- a/lib/ib/connection.rb +++ b/lib/ib/connection.rb @@ -482,14 +482,14 @@ def cancel_order *local_ids def start_reader if @reader_running @reader_thread - else # connected? # if called frm try_connection, the connected state is not set + 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 diff --git a/models/ib/option.rb b/models/ib/option.rb index 0217b7a..3fa1fef 100644 --- a/models/ib/option.rb +++ b/models/ib/option.rb @@ -81,6 +81,8 @@ def == other # # (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 ) @@ -91,7 +93,7 @@ def next_expiry d = Date.today 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" ) if exp[-2..-1] == "00" + error( "No suitable next expiry option found", :load ) if exp[-2..-1] == "00" end real_option else diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection-tools.rb index 38b1968..8176ff9 100644 --- a/plugins/ib/connection-tools.rb +++ b/plugins/ib/connection-tools.rb @@ -54,6 +54,7 @@ def check_connection retry rescue Workflow::NoTransitionAllowed logger.warn{ "Reconnect is not possible, actual state: #{workflow_state} cannot be reached after disconnection"} + raise end end unsubscribe z From 7b4aadbe4068e1213496aaa0ed4ccef6803438a5 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 7 Apr 2025 08:02:19 +0200 Subject: [PATCH 71/76] Updated Order-Conditions (testing required) --- conditions/ib/execution_condition.rb | 112 ---------------------- conditions/ib/margin_condition.rb | 89 +---------------- conditions/ib/order_condition.rb | 7 +- conditions/ib/percent_change_condition.rb | 5 +- conditions/ib/price_condition.rb | 2 +- conditions/ib/volume_condition.rb | 6 +- lib/ib-api.rb | 2 +- 7 files changed, 8 insertions(+), 215 deletions(-) diff --git a/conditions/ib/execution_condition.rb b/conditions/ib/execution_condition.rb index 8672cb4..0209d9e 100644 --- a/conditions/ib/execution_condition.rb +++ b/conditions/ib/execution_condition.rb @@ -1,7 +1,4 @@ module IB - - - class ExecutionCondition < OrderCondition using IB::Support # refine Array-method for decoding of IB-Messages @@ -30,114 +27,5 @@ def self.fabricate contract end - 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 - - - class VolumeCondition < OrderCondition - using IB::Support # 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 IB::Support # 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 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/conditions/ib/margin_condition.rb b/conditions/ib/margin_condition.rb index e1467ed..9a203b0 100644 --- a/conditions/ib/margin_condition.rb +++ b/conditions/ib/margin_condition.rb @@ -1,7 +1,5 @@ module IB - - class MarginCondition < OrderCondition using IB::Support # refine Array-method for decoding of IB-Messages @@ -15,11 +13,10 @@ 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 + super << self[:operator] << percent end def self.fabricate operator, percent error "Condition Operator has to be \">=\" or \"<=\" " unless ["<=", ">="].include? operator @@ -27,89 +24,5 @@ def self.fabricate operator, percent percent: percent end end - - - class VolumeCondition < OrderCondition - using IB::Support # 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 IB::Support # 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 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/conditions/ib/order_condition.rb b/conditions/ib/order_condition.rb index ea832c9..f5da9c2 100644 --- a/conditions/ib/order_condition.rb +++ b/conditions/ib/order_condition.rb @@ -1,13 +1,14 @@ 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 + 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" @@ -25,6 +26,4 @@ def serialize end end - - end # module diff --git a/conditions/ib/percent_change_condition.rb b/conditions/ib/percent_change_condition.rb index 3cbb874..0bb3e2e 100644 --- a/conditions/ib/percent_change_condition.rb +++ b/conditions/ib/percent_change_condition.rb @@ -1,15 +1,12 @@ 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 + 7 end def self.make buffer diff --git a/conditions/ib/price_condition.rb b/conditions/ib/price_condition.rb index 87b7c87..4003a31 100644 --- a/conditions/ib/price_condition.rb +++ b/conditions/ib/price_condition.rb @@ -12,7 +12,7 @@ def default_attributes end def condition_type - 1 + 1 end def self.make buffer diff --git a/conditions/ib/volume_condition.rb b/conditions/ib/volume_condition.rb index 6e04257..ebd28ef 100644 --- a/conditions/ib/volume_condition.rb +++ b/conditions/ib/volume_condition.rb @@ -1,8 +1,4 @@ module IB - - - - class VolumeCondition < OrderCondition using IB::Support # refine Array-method for decoding of IB-Messages include BaseProperties @@ -10,7 +6,7 @@ class VolumeCondition < OrderCondition prop :volume def condition_type - 6 + 6 end def self.make buffer diff --git a/lib/ib-api.rb b/lib/ib-api.rb index da451b4..93bbb48 100644 --- a/lib/ib-api.rb +++ b/lib/ib-api.rb @@ -39,6 +39,6 @@ loader.eager_load #require 'requires' require 'ib/contract.rb' -#require 'ib/order_condition.rb' +require 'ib/order_condition.rb' #IbRuby = Ib #IB = Ib From 05fcbd91e319cee8e4d2f7bc700f428106585147 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 28 Apr 2025 11:11:01 +0200 Subject: [PATCH 72/76] Default time-zones for contracts; using time-zones to fetch historical data --- lib/class_extensions.rb | 37 +++++++++++++++++++++++++++++++++---- models/ib/contract.rb | 16 ++++++++++++++++ plugins/ib/eod.rb | 18 ++++++++++++++---- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/lib/class_extensions.rb b/lib/class_extensions.rb index 9aecb8b..151a1b4 100644 --- a/lib/class_extensions.rb +++ b/lib/class_extensions.rb @@ -4,6 +4,8 @@ # # Define the method `count_duplicates` for Arrays # +require 'date' + module ClassExtensions module Array module DuplicatesCounter @@ -21,11 +23,37 @@ def as_table(&b) 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") - def to_ib - "#{year}#{sprintf("%02d", month)}#{sprintf("%02d", day)} " + - "#{sprintf("%02d", hour)}:#{sprintf("%02d", min)}:#{sprintf("%02d", sec)}" + # 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 @@ -108,6 +136,7 @@ def to_sup 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 diff --git a/models/ib/contract.rb b/models/ib/contract.rb index c2ec17c..c9caf8f 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -336,6 +336,22 @@ def crypto? # :nodoc: 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 diff --git a/plugins/ib/eod.rb b/plugins/ib/eod.rb index 3f2905d..add6c9c 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -81,6 +81,9 @@ def self.business_days_between(start_date, end_date) (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} @@ -96,7 +99,7 @@ def self.business_days_between(start_date, end_date) # # The parameter `:what` specifies the kind of received data. # - # Valid values: + # Valid values: ( /lib/ib/constants.rb --> DATA_TYPES ) # :trades, :midpoint, :bid, :ask, :bid_ask, # :historical_volatility, :option_implied_volatility, # :option_volume, :option_open_interest @@ -234,7 +237,7 @@ def eod start: nil, to: nil, duration: nil , what: :trades, polars: false end - get_bars(to.to_time.to_ib , normalize_duration[duration], barsize, what, polars) + get_bars( to.to_ib(time_zone) , normalize_duration[duration], barsize, what, polars ) end # def @@ -255,6 +258,15 @@ def from_csv file: nil 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 @@ -279,8 +291,6 @@ def get_bars(end_date_time, duration, bar_size, what_to_show, polars) # TWS Error 354: Requested market data is not subscribed. # TWS Error 162 # Historical Market Data Service error received.close - elsif msg.code.to_i == 2174 - tws.logger.info "Please switch to the \"10-19\"-Branch of the git-repository" end end From e370fd1680e4aa282bfbf93be71c7dc562668fab Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Thu, 1 May 2025 09:26:46 +0200 Subject: [PATCH 73/76] Contract#verify raises an error if the connection is interrupted --- plugins/ib/verify.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/ib/verify.rb b/plugins/ib/verify.rb index e7a7d01..c368d84 100644 --- a/plugins/ib/verify.rb +++ b/plugins/ib/verify.rb @@ -139,7 +139,7 @@ def _verify &b # :nodoc: ## 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.close + queue.push "InvalidContract" end when Messages::Incoming::ContractData if msg.request_id.to_i == message_id @@ -161,8 +161,17 @@ def _verify &b # :nodoc: # 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 - received_contracts << r + 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 From c0e5f58ac598902b44abad5ba361c580a4c1407a Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Mon, 5 May 2025 08:03:22 +0200 Subject: [PATCH 74/76] make Contract#verify more robust. --- plugins/ib/verify.rb | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/ib/verify.rb b/plugins/ib/verify.rb index c368d84..bda7f9d 100644 --- a/plugins/ib/verify.rb +++ b/plugins/ib/verify.rb @@ -75,7 +75,18 @@ def verify thread: nil, &b if thread Thread.new { _verify &b } else - _verify &b + i = 0 + begin + _verify &b + rescue IB::VerifyError + i += 1 + if i < 3 + sleep 1 + retry + else + raise + end + end end end # def @@ -91,17 +102,7 @@ def necessary_attributes # if the contract allows SMART routing end - # - # depreciated: Do not use anymore - def verify! - c = 0 - IB::Connection.logger.warn "Contract.verify! is depreciated. Use \"contract = contract.verify.first\" instead" - c= verify.first - self.attributes = c.invariant_attributes - self.contract_detail = c.contract_detail - self - end - + private # Base method to verify a contract From 80a2d4b0a93b56716c4673fc4b822f74d3ab66e9 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Wed, 7 May 2025 11:06:55 +0200 Subject: [PATCH 75/76] Disable verifying length of message buffer --- changelog.md | 7 +++++++ lib/ib/raw_message_parser.rb | 9 +++++---- plugins/ib/eod.rb | 9 ++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 6104030..30d8dbc 100644 --- a/changelog.md +++ b/changelog.md @@ -23,3 +23,10 @@ Changelog | 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/lib/ib/raw_message_parser.rb b/lib/ib/raw_message_parser.rb index 904fdbd..5c97083 100644 --- a/lib/ib/raw_message_parser.rb +++ b/lib/ib/raw_message_parser.rb @@ -89,10 +89,11 @@ def validate_message_footer(msg, _length) raise "Message has an invalid last byte. expecting \0, received: #{last_byte}" if last_byte != "\0" end - def validate_data_header(length) - return true if length <= 5000 - - raise 'Message is longer than sane max length' + 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/plugins/ib/eod.rb b/plugins/ib/eod.rb index add6c9c..d1fd36d 100644 --- a/plugins/ib/eod.rb +++ b/plugins/ib/eod.rb @@ -285,11 +285,14 @@ def get_bars(end_date_time, duration, bar_size, what_to_show, polars) received.push Time.now end b = tws.subscribe( IB::Messages::Incoming::Alert) do |msg| - if [321,162,200].include? msg.code - tws.logger.info msg.message + 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 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 From 44026167cdc5c256f8ce4d18c68df0c2a4127170 Mon Sep 17 00:00:00 2001 From: Hartmut Bischoff Date: Tue, 7 Oct 2025 07:59:56 +0200 Subject: [PATCH 76/76] Contract#multiplier needs to be decimal for adjustment to crypto --- lib/ib/messages/incoming/contract_data.rb | 2 +- lib/ib/support.rb | 2 +- models/ib/contract.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ib/messages/incoming/contract_data.rb b/lib/ib/messages/incoming/contract_data.rb index 55514e7..327fddb 100644 --- a/lib/ib/messages/incoming/contract_data.rb +++ b/lib/ib/messages/incoming/contract_data.rb @@ -18,7 +18,7 @@ module Incoming [:contract, :con_id, :int], [:contract_detail, :min_tick, :decimal], # [:contract_detail, :md_size_multiplier, :int], # Vers 10.12 not used anymore - [:contract, :multiplier, :int], + [:contract, :multiplier, :decimal], ## Crypto-option multipliers can be decimal [:contract_detail, :order_types, :string], [:contract_detail, :valid_exchanges, :string], [:contract_detail, :price_magnifier, :int], diff --git a/lib/ib/support.rb b/lib/ib/support.rb index f289ff6..65ff039 100644 --- a/lib/ib/support.rb +++ b/lib/ib/support.rb @@ -155,7 +155,7 @@ def read_contract # read a standard contract and return als hash expiry: read_string, strike: read_decimal, right: read_string, - multiplier: read_int, + multiplier: read_decimal, exchange: read_string, currency: read_string, local_symbol: read_string, diff --git a/models/ib/contract.rb b/models/ib/contract.rb index c9caf8f..ff7aec8 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -26,7 +26,7 @@ class Contract < IB::Base :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