From 92bf86ab8100f8897e8682fb4a86f0b3958ea6dc Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sat, 13 Apr 2024 06:53:27 +0200 Subject: [PATCH 01/13] init rubocop with only Naming/FileName activated --- .rubocop.yml | 22 ++++++++++++++++++++++ Gemfile | 5 +++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..6af245c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,22 @@ +# The behavior of RuboCop can be controlled via the .rubocop.yml +# configuration file. It makes it possible to enable/disable +# certain cops (checks) and to alter their behavior if they accept +# any parameters. The file can be placed either in your home +# directory or in some project directory. +# +# RuboCop will start looking for the configuration file in the directory +# where the inspected file is and continue its way up to the root directory. +# +# See https://docs.rubocop.org/rubocop/configuration + +require: + - rubocop-rspec + - rubocop-rake + +AllCops: + TargetRubyVersion: 3.2 + # NewCops: enable + DisabledByDefault: true + +Naming/FileName: + Enabled: true \ No newline at end of file diff --git a/Gemfile b/Gemfile index 345fbcc..b2e3202 100644 --- a/Gemfile +++ b/Gemfile @@ -7,11 +7,12 @@ gemspec # gem 'value_semantics' # models, future plans - - gem 'rspec' gem 'rspec-its' gem 'rspec-given' gem 'rspec-collection_matchers' +gem 'rubocop' +gem 'rubocop-rake' +gem 'rubocop-rspec' gem 'guard' gem 'guard-rspec' From f53a05802eefe793b0ae131ec3ce07e26876183c Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sat, 13 Apr 2024 06:53:50 +0200 Subject: [PATCH 02/13] replace dashes by underscores in filenames --- README.md | 16 ++++++++-------- bin/console | 6 +++--- lib/ib/connection.rb | 2 +- lib/ib/messages/incoming/position_data.rb | 2 +- lib/ib/plugins.rb | 4 ++-- lib/{ib-api.rb => ib_api.rb} | 2 +- models/ib/contract.rb | 2 +- .../{connection-tools.rb => connection_tools.rb} | 0 .../{managed-accounts.rb => managed_accounts.rb} | 6 +++--- plugins/ib/{market-price.rb => market_price.rb} | 4 ++-- plugins/ib/{option-chain.rb => option_chain.rb} | 2 +- .../{order-prototypes.rb => order_prototypes.rb} | 0 .../{all-in-one.rb => all_in_one.rb} | 0 plugins/ib/order_prototypes/stop.rb | 2 +- ...of-expiring.rb => probability_of_expiring.rb} | 0 ...spread-prototypes.rb => spread_prototypes.rb} | 2 +- .../{stock-spread.rb => stock_spread.rb} | 0 spec/spec_helper.rb | 2 +- 18 files changed, 26 insertions(+), 26 deletions(-) rename lib/{ib-api.rb => ib_api.rb} (96%) rename plugins/ib/{connection-tools.rb => connection_tools.rb} (100%) rename plugins/ib/{managed-accounts.rb => managed_accounts.rb} (98%) rename plugins/ib/{market-price.rb => market_price.rb} (98%) rename plugins/ib/{option-chain.rb => option_chain.rb} (99%) rename plugins/ib/{order-prototypes.rb => order_prototypes.rb} (100%) rename plugins/ib/order_prototypes/{all-in-one.rb => all_in_one.rb} (100%) rename plugins/ib/{probability-of-expiring.rb => probability_of_expiring.rb} (100%) rename plugins/ib/{spread-prototypes.rb => spread_prototypes.rb} (97%) rename plugins/ib/spread_prototypes/{stock-spread.rb => stock_spread.rb} (100%) diff --git a/README.md b/README.md index d963e8d..eb304c6 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ In its plain vanilla usage, it just exchanges messages with the TWS. Any respons It needs just a few lines of code to place an order ```ruby -require 'ib-api' +require 'ib_api' # connect with default parameters ib = IB::Connection.new @@ -56,7 +56,7 @@ puts ib.recieved[:OrderStatus].to_human **IB-API** ships with simple plugins to facilitate automations ```ruby -require 'ib-api' +require 'ib_api' # connect with default parameters ib = IB::Connection.new do | c | c.activate_plugin "verify" @@ -69,15 +69,15 @@ puts g.verify.first.attributes Currently implemented plugins -* connection-tools: ensure that a connection is established and active +* 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 +* 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 -* 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 +* 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 ## Minimal TWS-Version diff --git a/bin/console b/bin/console index c99ba82..66b0ad9 100755 --- a/bin/console +++ b/bin/console @@ -10,7 +10,7 @@ require 'bundler/setup' require 'yaml' -require 'ib-api' +require 'ib_api' class Array # enables calling members of an array. which are hashes by it name @@ -64,9 +64,9 @@ read_yml = -> (key) do ## 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 'connection_tools' c.activate_plugin 'verify' - c.activate_plugin 'managed-accounts' + 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 }} diff --git a/lib/ib/connection.rb b/lib/ib/connection.rb index bcba9a7..f30277d 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.include? "connection-tools" + if @plugins.include? "connection_tools" safe_connect else connect() # connect implies requesting NextValidId diff --git a/lib/ib/messages/incoming/position_data.rb b/lib/ib/messages/incoming/position_data.rb index 586c185..734cbc4 100644 --- a/lib/ib/messages/incoming/position_data.rb +++ b/lib/ib/messages/incoming/position_data.rb @@ -12,7 +12,7 @@ module Incoming [:price, :decimal] ) do # def to_human - " #{contract.to_human} ( Amount #{position}) : Market-Price #{price} >" + " #{contract.to_human} ( Amount #{position}) : market_price #{price} >" end diff --git a/lib/ib/plugins.rb b/lib/ib/plugins.rb index d5293f3..64a051f 100644 --- a/lib/ib/plugins.rb +++ b/lib/ib/plugins.rb @@ -2,9 +2,9 @@ module IB module Plugins def activate_plugin name unless @plugins.include? name - # root= base directory of the ib-api source + # root= base directory of the ib_api source root= Pathname.new( File.expand_path("../../../", __FILE__ )) - # plugins are defined in ib-api/plugins/ib + # plugins are defined in ib_api/plugins/ib filename= root + "plugins/ib/#{name}.rb" if filename.exist? if require filename diff --git a/lib/ib-api.rb b/lib/ib_api.rb similarity index 96% rename from lib/ib-api.rb rename to lib/ib_api.rb index 3c25ca1..d09d742 100644 --- a/lib/ib-api.rb +++ b/lib/ib_api.rb @@ -17,7 +17,7 @@ #loader = Zeitwerk::Loader.new loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false) loader.ignore("#{__dir__}/server_versions.rb") -loader.ignore("#{__dir__}/ib-api.rb") +loader.ignore("#{__dir__}/ib_api.rb") loader.ignore("#{__dir__}/ib/contract.rb") loader.ignore("#{__dir__}/ib/constants.rb") loader.ignore("#{__dir__}/ib/errors.rb") diff --git a/models/ib/contract.rb b/models/ib/contract.rb index 8cb67b5..847ba57 100644 --- a/models/ib/contract.rb +++ b/models/ib/contract.rb @@ -400,7 +400,7 @@ def expiry end -# is read by Account#PlaceOrder to set requirements for contract-types, as NonGuaranteed for stock-spreads +# is read by Account#PlaceOrder to set requirements for contract-types, as NonGuaranteed for stock_spreads def order_requirements Hash.new end diff --git a/plugins/ib/connection-tools.rb b/plugins/ib/connection_tools.rb similarity index 100% rename from plugins/ib/connection-tools.rb rename to plugins/ib/connection_tools.rb diff --git a/plugins/ib/managed-accounts.rb b/plugins/ib/managed_accounts.rb similarity index 98% rename from plugins/ib/managed-accounts.rb rename to plugins/ib/managed_accounts.rb index 2dde29b..9641893 100644 --- a/plugins/ib/managed-accounts.rb +++ b/plugins/ib/managed_accounts.rb @@ -26,7 +26,7 @@ module IB Standard usage ib = Connection.new connect: false do | c | - c.activate_plugin 'managed-accounts' + c.activate_plugin 'managed_accounts' c.initialize_managed_accounts c.get_account_data end @@ -39,7 +39,7 @@ module ManagedAccounts --------------------------- InitializeManageAccounts ---------------------------------- If initiated with the parameter `force: true`, a reconnect is performed to initiate the -transmission of available managed-accounts. +transmission of available managed_accounts. =end def initialize_managed_accounts( force: false ) @@ -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() diff --git a/plugins/ib/market-price.rb b/plugins/ib/market_price.rb similarity index 98% rename from plugins/ib/market-price.rb rename to plugins/ib/market_price.rb index 61893de..9824f69 100644 --- a/plugins/ib/market-price.rb +++ b/plugins/ib/market_price.rb @@ -1,7 +1,7 @@ module IB module MarketPrice -# Ask for the Market-Price +# Ask for the market_price # # For valid contracts, either bid/ask or last_price and close_price are transmitted. # @@ -21,7 +21,7 @@ module MarketPrice # (volatile, ie. data are not preserved when the Object is copied) # #Example: IB::Stock.new(symbol: :ge).market_price - # returns the current 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 diff --git a/plugins/ib/option-chain.rb b/plugins/ib/option_chain.rb similarity index 99% rename from plugins/ib/option-chain.rb rename to plugins/ib/option_chain.rb index 7399dca..2930d52 100644 --- a/plugins/ib/option-chain.rb +++ b/plugins/ib/option_chain.rb @@ -158,7 +158,7 @@ def otm_options count: 5, right: :put, ref_price: :request, sort: :strike, exc end # module Connection.current.activate_plugin 'verify' - Connection.current.activate_plugin 'market-price' + Connection.current.activate_plugin 'market_price' class Contract include OptionChain diff --git a/plugins/ib/order-prototypes.rb b/plugins/ib/order_prototypes.rb similarity index 100% rename from plugins/ib/order-prototypes.rb rename to plugins/ib/order_prototypes.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/stop.rb b/plugins/ib/order_prototypes/stop.rb index 2bf2bb0..c0f31e0 100644 --- a/plugins/ib/order_prototypes/stop.rb +++ b/plugins/ib/order_prototypes/stop.rb @@ -109,7 +109,7 @@ def aliases end def requirements - ## usualy the trail_stop_price is the market-price minus(plus) the trailing_amount + ## 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 diff --git a/plugins/ib/probability-of-expiring.rb b/plugins/ib/probability_of_expiring.rb similarity index 100% rename from plugins/ib/probability-of-expiring.rb rename to plugins/ib/probability_of_expiring.rb diff --git a/plugins/ib/spread-prototypes.rb b/plugins/ib/spread_prototypes.rb similarity index 97% rename from plugins/ib/spread-prototypes.rb rename to plugins/ib/spread_prototypes.rb index 281148e..94c5c69 100644 --- a/plugins/ib/spread-prototypes.rb +++ b/plugins/ib/spread_prototypes.rb @@ -63,7 +63,7 @@ def parameters end end Connection.current.activate_plugin "verify" - [:straddle, :strangle, :vertical, :calendar, :"stock-spread", :butterfly].each do | pt | + [:straddle, :strangle, :vertical, :calendar, :"stock_spread", :butterfly].each do | pt | Connection.current.activate_plugin "spread_prototypes/#{pt.to_s}" end end diff --git a/plugins/ib/spread_prototypes/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/spec/spec_helper.rb b/spec/spec_helper.rb index 5ef2f3f..c783110 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,7 +5,7 @@ require 'rspec/its' require 'rspec/given' require 'rspec/collection_matchers' -require 'ib-api' +require 'ib_api' require 'pp' require 'yaml' From fe778f131c27460b8dcd12f22814898fba538899 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sat, 13 Apr 2024 21:00:33 +0200 Subject: [PATCH 03/13] adds missing `end` to request_real_time_bars.rb --- .../outgoing/request_real_time_bars.rb | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/ib/messages/outgoing/request_real_time_bars.rb b/lib/ib/messages/outgoing/request_real_time_bars.rb index 743ab04..9f354c7 100644 --- a/lib/ib/messages/outgoing/request_real_time_bars.rb +++ b/lib/ib/messages/outgoing/request_real_time_bars.rb @@ -36,13 +36,16 @@ def parse data 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 - ] + [ + 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 - + end + end + end # RequestRealTimeBars +end From a626a276025062c776ff3c853e973d75fe9b20a4 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Sun, 14 Apr 2024 20:44:28 +0200 Subject: [PATCH 04/13] introduce spec covering methods atm_options, itm_options & otm_options --- spec/plugins/ib/option_chain_spec.rb | 231 +++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 spec/plugins/ib/option_chain_spec.rb diff --git a/spec/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb new file mode 100644 index 0000000..14747f5 --- /dev/null +++ b/spec/plugins/ib/option_chain_spec.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require 'main_helper' + +RSpec.describe 'IB::OptionChain' do + before(:all) do + establish_connection + IB::Connection.current.activate_plugin 'option_chain' + end + + after(:all) do + close_connection + end + + context 'when contract is_a Stock' do + let(:contract) { IB::Stock.new(symbol: 'GE') } + + it 'returns correctly ATM put options' do + result = contract.atm_options + expect(result.keys).to all(be_a Integer) + expect(result.keys.size).to be > 1 + first_atm_expiry_date_key = result.keys.first + first_atm_expiry_date = Date.strptime(first_atm_expiry_date_key.to_s, '%y%m') + expect(result[first_atm_expiry_date_key].size).to eq(1) + + first_atm_option = result[first_atm_expiry_date_key].first + expect(first_atm_option).to be_a(IB::Option) + expect(first_atm_option.attributes).to include({ + symbol: 'GE', + last_trading_day: /#{first_atm_expiry_date.strftime('%Y-%m-')}/, + right: 'P', + exchange: be_a(String), + local_symbol: /GE\s+#{first_atm_expiry_date_key}/, + trading_class: 'GE', + multiplier: 100 + }) + end + + it 'returns correctly ATM call options' do + result = contract.atm_options(right: :call) + expect(result.keys).to all(be_a Integer) + expect(result.keys.size).to be > 1 + first_atm_expiry_date_key = result.keys.first + first_atm_expiry_date = Date.strptime(first_atm_expiry_date_key.to_s, '%y%m') + expect(result[first_atm_expiry_date_key].size).to eq(1) + + first_atm_option = result[first_atm_expiry_date_key].first + expect(first_atm_option).to be_a(IB::Option) + expect(first_atm_option.attributes).to include({ + symbol: 'GE', + last_trading_day: /#{first_atm_expiry_date.strftime('%Y-%m-')}/, + right: 'C', + exchange: be_a(String), + local_symbol: /GE\s+#{first_atm_expiry_date_key}/, + trading_class: 'GE', + multiplier: 100 + }) + end + + it 'does not find ATM options for far away ref_price' do + result = contract.atm_options(ref_price: 0.0) + expect(result.keys).to all(be_a Integer) + expect(result.keys.size).to be > 1 + + first_atm_expiry_date_key = result.keys.first + expect(result[first_atm_expiry_date_key]).to be_empty + end + + it 'returns correctly OTM put options' do + result = contract.otm_options + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 OTM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_otm_option = result[strike_price].first + expect(first_otm_option).to be_a(IB::Option) + expect(first_otm_option.attributes).to include({ + symbol: 'GE', + right: 'P', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class: 'GE', + multiplier: 100 + }) + + expect(first_otm_option.strike).to be > result[result.keys[1]].first.strike + end + + it 'returns correctly OTM call options' do + result = contract.otm_options(right: :call) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 OTM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_otm_option = result[strike_price].first + expect(first_otm_option).to be_a(IB::Option) + expect(first_otm_option.attributes).to include({ + symbol: 'GE', + right: 'C', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class: 'GE', + multiplier: 100 + }) + + expect(first_otm_option.strike).to be < result[result.keys[1]].first.strike + end + + it 'sorts OTM options by expiry' do + result = contract.otm_options(sort: :expiry) + expect(result.keys).to all(be_a Integer) + expect(result.keys.size).to eq(12) + first_otm_expiry_date_key = result.keys.first + first_otm_expiry_date = Date.strptime(first_otm_expiry_date_key.to_s, '%y%m') + expect(result[first_otm_expiry_date_key].size).to be_positive + + first_otm_option = result[first_otm_expiry_date_key].first + expect(first_otm_option).to be_a(IB::Option) + expect(first_otm_option.attributes).to include({ + symbol: 'GE', + last_trading_day: /#{first_otm_expiry_date.strftime('%Y-%m-')}/, + right: 'P', + exchange: be_a(String), + local_symbol: /GE\s+#{first_otm_expiry_date_key}/, + trading_class: 'GE', + multiplier: 100 + }) + end + + it 'limits OTM put options to 3' do + result = contract.otm_options(count: 3) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(4) # ATM + 3 OTM + end + + it 'limits OTM call options to 3' do + result = contract.otm_options(count: 3, right: :call) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(4) # ATM + 3 OTM + end + + it 'does not find OTM options for far away ref_price' do + result = contract.otm_options(ref_price: 0.0) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(0) + end + + it 'returns correctly ITM put options' do + result = contract.itm_options + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 ITM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_itm_option = result[strike_price].first + expect(first_itm_option).to be_a(IB::Option) + expect(first_itm_option.attributes).to include({ + symbol: 'GE', + right: 'P', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class: 'GE', + multiplier: 100 + }) + + expect(first_itm_option.strike).to be < result[result.keys[1]].first.strike + end + + it 'returns correctly ITM call options' do + result = contract.itm_options(right: :call) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 ITM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_itm_option = result[strike_price].first + expect(first_itm_option).to be_a(IB::Option) + expect(first_itm_option.attributes).to include({ + symbol: 'GE', + right: 'C', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class: 'GE', + multiplier: 100 + }) + + expect(first_itm_option.strike).to be > result[result.keys[1]].first.strike + end + + it 'sorts ITM options by expiry' do + result = contract.itm_options(sort: :expiry) + expect(result.keys).to all(be_a Integer) + expect(result.keys.size).to eq(12) + first_itm_expiry_date_key = result.keys.first + first_itm_expiry_date = Date.strptime(first_itm_expiry_date_key.to_s, '%y%m') + expect(result[first_itm_expiry_date_key].size).to be_positive + + first_itm_option = result[first_itm_expiry_date_key].first + expect(first_itm_option).to be_a(IB::Option) + expect(first_itm_option.attributes).to include({ + symbol: 'GE', + last_trading_day: /#{first_itm_expiry_date.strftime('%Y-%m-')}/, + right: 'P', + exchange: be_a(String), + local_symbol: /GE\s+#{first_itm_expiry_date_key}/, + trading_class: 'GE', + multiplier: 100 + }) + end + + it 'limits ITM put options to 3' do + result = contract.itm_options(count: 3) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(4) # ATM + 3 ITM + end + + it 'limits ITM call options to 3' do + result = contract.itm_options(count: 3, right: :call) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(4) # ATM + 3 ITM + end + + it 'does not find ITM options for far away ref_price' do + result = contract.itm_options(ref_price: 10_000.0) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(0) + end + end +end From b78fd665f58cfaf2b307398fa5299c7a3ab58c5c Mon Sep 17 00:00:00 2001 From: psmandzich Date: Mon, 15 Apr 2024 16:36:02 +0200 Subject: [PATCH 05/13] extract inline lambda helpers to methods --- plugins/ib/option_chain.rb | 61 +++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 2930d52..ba87dda 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -90,33 +90,12 @@ 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 - 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 + Connection.logger.info @option_chain_definition.inspect if sort == :strike - options_by_strike[ requested_strikes ] + options_by_strike(requested_strikes, monthly_expirations, right) else - options_by_expiry[ requested_strikes ] + options_by_expiry(requested_strikes, monthly_expirations, right) end else Connection.logger.error "#{to_human} ::No Options available" @@ -155,6 +134,40 @@ def otm_options count: 5, right: :put, ref_price: :request, sort: :strike, exc end end end + + private + + def option_prototype(last_trading_day, strike, right) + IB::Option.new( + symbol:, + exchange: @option_chain_definition[:exchange], + trading_class: @option_chain_definition[:trading_class], + multiplier: @option_chain_definition[:multiplier], + currency: currency, + last_trading_day:, + strike:, + right: + ).verify &.first + end + + def options_by_expiry(strikes, expirations, right) + # Array: [ yymm -> Options] prepares for the correct conversion to a Hash + expirations.map do |expiration_date| + [ + expiration_date.strftime('%y%m').to_i, + strikes.map { |strike| option_prototype(expiration_date, strike, right) }.compact + ] + end.to_h + end + + def options_by_strike(strikes, expirations, right) + strikes.map do |strike| + [ + strike, + expirations.map { |expiration_date| option_prototype(expiration_date, strike, right) }.compact + ] + end.to_h + end end # module Connection.current.activate_plugin 'verify' From eedf9c8b49c66f17bd1d01a7010df8a7bda459a6 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Mon, 15 Apr 2024 21:10:06 +0200 Subject: [PATCH 06/13] use whole timestamp in expiration hash --- plugins/ib/option_chain.rb | 2 +- spec/plugins/ib/option_chain_spec.rb | 34 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index ba87dda..690e1d1 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -154,7 +154,7 @@ def options_by_expiry(strikes, expirations, right) # Array: [ yymm -> Options] prepares for the correct conversion to a Hash expirations.map do |expiration_date| [ - expiration_date.strftime('%y%m').to_i, + expiration_date.strftime('%Y-%m-%d'), strikes.map { |strike| option_prototype(expiration_date, strike, right) }.compact ] end.to_h diff --git a/spec/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb index 14747f5..750140c 100644 --- a/spec/plugins/ib/option_chain_spec.rb +++ b/spec/plugins/ib/option_chain_spec.rb @@ -17,20 +17,20 @@ it 'returns correctly ATM put options' do result = contract.atm_options - expect(result.keys).to all(be_a Integer) + expect(result.keys).to all(be_a String) expect(result.keys.size).to be > 1 first_atm_expiry_date_key = result.keys.first - first_atm_expiry_date = Date.strptime(first_atm_expiry_date_key.to_s, '%y%m') + first_atm_expiry_date = Date.parse(first_atm_expiry_date_key) expect(result[first_atm_expiry_date_key].size).to eq(1) first_atm_option = result[first_atm_expiry_date_key].first expect(first_atm_option).to be_a(IB::Option) expect(first_atm_option.attributes).to include({ symbol: 'GE', - last_trading_day: /#{first_atm_expiry_date.strftime('%Y-%m-')}/, + last_trading_day: first_atm_expiry_date_key, right: 'P', exchange: be_a(String), - local_symbol: /GE\s+#{first_atm_expiry_date_key}/, + local_symbol: /GE\s+#{first_atm_expiry_date_.strftime('%y%m%d')}/, trading_class: 'GE', multiplier: 100 }) @@ -38,20 +38,20 @@ it 'returns correctly ATM call options' do result = contract.atm_options(right: :call) - expect(result.keys).to all(be_a Integer) + expect(result.keys).to all(be_a String) expect(result.keys.size).to be > 1 first_atm_expiry_date_key = result.keys.first - first_atm_expiry_date = Date.strptime(first_atm_expiry_date_key.to_s, '%y%m') + first_atm_expiry_date = Date.parse(first_atm_expiry_date_key) expect(result[first_atm_expiry_date_key].size).to eq(1) first_atm_option = result[first_atm_expiry_date_key].first expect(first_atm_option).to be_a(IB::Option) expect(first_atm_option.attributes).to include({ symbol: 'GE', - last_trading_day: /#{first_atm_expiry_date.strftime('%Y-%m-')}/, + last_trading_day: first_atm_expiry_date_key, right: 'C', exchange: be_a(String), - local_symbol: /GE\s+#{first_atm_expiry_date_key}/, + local_symbol: /GE\s+#{first_atm_expiry_date.strftime('%y%m%d')}/, trading_class: 'GE', multiplier: 100 }) @@ -59,7 +59,7 @@ it 'does not find ATM options for far away ref_price' do result = contract.atm_options(ref_price: 0.0) - expect(result.keys).to all(be_a Integer) + expect(result.keys).to all(be_a String) expect(result.keys.size).to be > 1 first_atm_expiry_date_key = result.keys.first @@ -110,20 +110,20 @@ it 'sorts OTM options by expiry' do result = contract.otm_options(sort: :expiry) - expect(result.keys).to all(be_a Integer) + expect(result.keys).to all(be_a String) expect(result.keys.size).to eq(12) first_otm_expiry_date_key = result.keys.first - first_otm_expiry_date = Date.strptime(first_otm_expiry_date_key.to_s, '%y%m') + first_otm_expiry_date = Date.parse(first_otm_expiry_date_key) expect(result[first_otm_expiry_date_key].size).to be_positive first_otm_option = result[first_otm_expiry_date_key].first expect(first_otm_option).to be_a(IB::Option) expect(first_otm_option.attributes).to include({ symbol: 'GE', - last_trading_day: /#{first_otm_expiry_date.strftime('%Y-%m-')}/, + last_trading_day: first_otm_expiry_date_key, right: 'P', exchange: be_a(String), - local_symbol: /GE\s+#{first_otm_expiry_date_key}/, + local_symbol: /GE\s+#{first_otm_expiry_date.strftime('%y%m%d')}/, trading_class: 'GE', multiplier: 100 }) @@ -191,20 +191,20 @@ it 'sorts ITM options by expiry' do result = contract.itm_options(sort: :expiry) - expect(result.keys).to all(be_a Integer) + expect(result.keys).to all(be_a String) expect(result.keys.size).to eq(12) first_itm_expiry_date_key = result.keys.first - first_itm_expiry_date = Date.strptime(first_itm_expiry_date_key.to_s, '%y%m') + first_itm_expiry_date = Date.parse(first_itm_expiry_date_key) expect(result[first_itm_expiry_date_key].size).to be_positive first_itm_option = result[first_itm_expiry_date_key].first expect(first_itm_option).to be_a(IB::Option) expect(first_itm_option.attributes).to include({ symbol: 'GE', - last_trading_day: /#{first_itm_expiry_date.strftime('%Y-%m-')}/, + last_trading_day: first_itm_expiry_date_key, right: 'P', exchange: be_a(String), - local_symbol: /GE\s+#{first_itm_expiry_date_key}/, + local_symbol: /GE\s+#{first_itm_expiry_date.strftime('%y%m%d')}/, trading_class: 'GE', multiplier: 100 }) From 7b1f33b3527c066815486b53f93b6ff7d0923fad Mon Sep 17 00:00:00 2001 From: psmandzich Date: Tue, 16 Apr 2024 15:33:24 +0200 Subject: [PATCH 07/13] extract monthly expiration selection to method --- lib/ib/base_properties.rb | 2 ++ plugins/ib/option_chain.rb | 14 +++++++++++--- spec/plugins/ib/option_chain_spec.rb | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/ib/base_properties.rb b/lib/ib/base_properties.rb index 5d9ba0a..2697153 100644 --- a/lib/ib/base_properties.rb +++ b/lib/ib/base_properties.rb @@ -1,4 +1,6 @@ #require 'active_support/hash_with_indifferent_access' +require "active_support" +require 'active_support/core_ext/date/calculations' module IB diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 690e1d1..969b93c 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -89,13 +89,14 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', end # third Friday of a month - monthly_expirations = @option_chain_definition[:expirations].find_all {|y| (15..21).include? y.day } + requested_expiration = @option_chain_definition[:expirations] + .select { |expiration| monthly_expiration?(expiration) } Connection.logger.info @option_chain_definition.inspect if sort == :strike - options_by_strike(requested_strikes, monthly_expirations, right) + options_by_strike(requested_strikes, requested_expiration, right) else - options_by_expiry(requested_strikes, monthly_expirations, right) + options_by_expiry(requested_strikes, requested_expiration, right) end else Connection.logger.error "#{to_human} ::No Options available" @@ -168,6 +169,13 @@ def options_by_strike(strikes, expirations, right) ] end.to_h end + + def monthly_expiration?(last_trading_day) + first_day_of_month = last_trading_day.beginning_of_month + third_friday = first_day_of_month.advance(days: ((5 - first_day_of_month.wday) % 7), weeks: 2) + + last_trading_day == third_friday + end end # module Connection.current.activate_plugin 'verify' diff --git a/spec/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb index 750140c..3bba61f 100644 --- a/spec/plugins/ib/option_chain_spec.rb +++ b/spec/plugins/ib/option_chain_spec.rb @@ -30,7 +30,7 @@ last_trading_day: first_atm_expiry_date_key, right: 'P', exchange: be_a(String), - local_symbol: /GE\s+#{first_atm_expiry_date_.strftime('%y%m%d')}/, + local_symbol: /GE\s+#{first_atm_expiry_date.strftime('%y%m%d')}/, trading_class: 'GE', multiplier: 100 }) From a0dd58ab5254f29b995e3b8c6957b2bc88773380 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Tue, 16 Apr 2024 15:48:14 +0200 Subject: [PATCH 08/13] handle all incoming OptionChainDefinition messages before unsubscribing --- plugins/ib/option_chain.rb | 45 +++++++++++++++----------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 969b93c..c81fdb4 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -27,41 +27,32 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', # ----------------------------------------------------------------------------------------------------- # 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 + sub_sdop = ib.subscribe(:SecurityDefinitionOptionParameterEnd) do |msg| + finalize.close if msg.request_id == my_req + end + sub_ocd = ib.subscribe(:OptionChainDefinition) do |msg| + finalize.push(msg.data) if msg.request_id == my_req 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 + contract = verify.first # ensure a complete set of attributes + my_req = ib.send_message :RequestOptionChainDefinition, + con_id: contract.con_id, + symbol: contract.symbol, + exchange: contract.sec_type == :future ? contract.exchange : "", # BOX,CBOE', + sec_type: contract[:sec_type] + until finalize.closed? + @option_chain_definition << finalize.pop + end ib.unsubscribe sub_sdop, sub_ocd else Connection.logger.info { "#{to_human} : using cached data" } end + + @option_chain_definition = @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || + @option_chain_definition.first + # ----------------------------------------------------------------------------------------------------- # select values and assign to options # From 4357d7068737bcf4a05171083dd790ccbf7abd24 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Wed, 17 Apr 2024 16:00:23 +0200 Subject: [PATCH 09/13] simplify wrapper methods of option_chain --- plugins/ib/option_chain.rb | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index c81fdb4..193430c 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -96,33 +96,30 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', 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 | + def atm_options(ref_price: :request, right: :put, **params) + option_chain(right:, 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 | + # return InTheMoneyOptions + def itm_options(count: 5, right: :put, ref_price: :request, sort: :strike, **params) + option_chain(right:, ref_price:, sort:, **params) do |chain| if right == :put - above_market_price_strikes = chain[1][0..count-1] + chain[1][0..count-1] # above market price else - below_market_price_strikes = chain[-1][-count..-1].reverse - end # branch + chain[-1][-count..-1].reverse # below market price + end end - end # def + end # 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 | + def otm_options(count: 5, right: :put, ref_price: :request, sort: :strike, **params) + option_chain(right:, ref_price:, sort:, **params ) do |chain| if right == :put - # puts "Chain: #{chain}" - below_market_price_strikes = chain[-1][-count..-1].reverse + chain[-1][-count..-1].reverse # below market price else - above_market_price_strikes = chain[1][0..count-1] + chain[1][0..count-1] # above market price end end end From d5218f0b07f6c1054863ec015df0b59687b9cdbc Mon Sep 17 00:00:00 2001 From: psmandzich Date: Wed, 17 Apr 2024 16:04:34 +0200 Subject: [PATCH 10/13] fix trading_class handling to retrieve SPXW options --- plugins/ib/option_chain.rb | 8 ++- spec/plugins/ib/option_chain_spec.rb | 96 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 193430c..d20b24e 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -39,6 +39,7 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', con_id: contract.con_id, symbol: contract.symbol, exchange: contract.sec_type == :future ? contract.exchange : "", # BOX,CBOE', + trading_class:, sec_type: contract[:sec_type] until finalize.closed? @@ -49,8 +50,11 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', Connection.logger.info { "#{to_human} : using cached data" } end - - @option_chain_definition = @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || + @option_chain_definition.compact! + Connection.logger.info { @option_chain_definition.map { |x| x.slice(:trading_class, :exchange)} } + @option_chain_definition = @option_chain_definition.find { |x| (trading_class.blank? || x[:trading_class] == trading_class) && (exchange.blank? || x[:exchange] == exchange) } || + @option_chain_definition.find { |x| x[:exchange] == contract.exchange && x[:trading_class] == contract.trading_class } || + @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || @option_chain_definition.first # ----------------------------------------------------------------------------------------------------- diff --git a/spec/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb index 3bba61f..29394ea 100644 --- a/spec/plugins/ib/option_chain_spec.rb +++ b/spec/plugins/ib/option_chain_spec.rb @@ -228,4 +228,100 @@ expect(result.keys.size).to eq(0) end end + + context 'when contract is_a Index' do + let!(:contract) { IB::Index.new(symbol: 'SPX', currency: 'USD', exchange: 'CBOE').verify.first } + + context 'when trading_class is set' do + let(:trading_class) { 'SPXW' } + + it 'returns correctly ATM put options' do + result = contract.atm_options(trading_class:) + expect(result.keys).to all(be_a String) + expect(result.keys.size).to be > 1 + first_atm_expiry_date_key = result.keys.first + first_atm_expiry_date = Date.parse(first_atm_expiry_date_key) + expect(result[first_atm_expiry_date_key].size).to eq(1) + + first_atm_option = result[first_atm_expiry_date_key].first + expect(first_atm_option).to be_a(IB::Option) + expect(first_atm_option.attributes).to include( + { + symbol: contract.symbol, + last_trading_day: first_atm_expiry_date_key, + right: 'P', + exchange: be_a(String), + local_symbol: /#{contract.trading_class}\s+#{first_atm_expiry_date.strftime('%y%m%d')}/, + trading_class:, + multiplier: 100 + } + ) + end + + it 'returns correctly OTM put options' do + result = contract.otm_options(trading_class:) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 OTM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_otm_option = result[strike_price].first + expect(first_otm_option).to be_a(IB::Option) + expect(first_otm_option.attributes).to include({ + symbol: contract.symbol, + right: 'P', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class:, + multiplier: 100 + }) + + expect(first_otm_option.strike).to be > result[result.keys[1]].first.strike + end + + it 'returns correctly OTM call options' do + result = contract.otm_options(right: :call, trading_class:) + expect(result.keys).to all(be_a BigDecimal) + expect(result.keys.size).to eq(6) # ATM + 5 OTM + strike_price = result.keys.first + expect(result[strike_price].size).to be_positive + + first_otm_option = result[strike_price].first + expect(first_otm_option).to be_a(IB::Option) + expect(first_otm_option.attributes).to include({ + symbol: contract.symbol, + right: 'C', + exchange: be_a(String), + strike: strike_price.to_f, + trading_class:, + multiplier: 100 + }) + + expect(first_otm_option.strike).to be < result[result.keys[1]].first.strike + end + + it 'sorts ITM options by expiry' do + result = contract.itm_options(sort: :expiry, trading_class:) + expect(result.keys).to all(be_a String) + expect(result.keys.size).to eq(5) + first_itm_expiry_date_key = result.keys.first + first_itm_expiry_date = Date.parse(first_itm_expiry_date_key) + expect(result[first_itm_expiry_date_key].size).to be_positive + + first_itm_option = result[first_itm_expiry_date_key].first + expect(first_itm_option).to be_a(IB::Option) + expect(first_itm_option.attributes).to include( + { + symbol: contract.symbol, + last_trading_day: first_itm_expiry_date_key, + right: 'P', + exchange: be_a(String), + local_symbol: /#{contract.trading_class}\s+#{first_itm_expiry_date.strftime('%y%m%d')}/, + trading_class:, + multiplier: 100 + } + ) + end + end + end end From 2990e528b374b0d2669a930630c838374e7142c7 Mon Sep 17 00:00:00 2001 From: psmandzich Date: Wed, 17 Apr 2024 16:24:27 +0200 Subject: [PATCH 11/13] introduce ability to limit the expirations returned by option_chain --- plugins/ib/option_chain.rb | 12 +++++++++--- spec/plugins/ib/option_chain_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index d20b24e..7111984 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -12,7 +12,7 @@ module OptionChain ### 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 + def option_chain(ref_price: :request, right: :put, sort: :strike, limit_expirations: nil, exchange: nil, trading_class: nil) ib = Connection.current @@ -84,8 +84,14 @@ def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', end # third Friday of a month - requested_expiration = @option_chain_definition[:expirations] - .select { |expiration| monthly_expiration?(expiration) } + requested_expiration = case limit_expirations + when :monthly + @option_chain_definition[:expirations].select { |expiration| monthly_expiration?(expiration) } + when :next + @option_chain_definition[:expirations].first(1) + else + @option_chain_definition[:expirations] + end Connection.logger.info @option_chain_definition.inspect if sort == :strike diff --git a/spec/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb index 29394ea..3f9b299 100644 --- a/spec/plugins/ib/option_chain_spec.rb +++ b/spec/plugins/ib/option_chain_spec.rb @@ -66,6 +66,27 @@ expect(result[first_atm_expiry_date_key]).to be_empty end + it 'returns only the next expiry' do + result = contract.atm_options(sort: :expiry, limit_expirations: :next) + expect(result.keys).to all(be_a String) + expect(result.keys.size).to eq(1) + + expect(result.keys.first).to eq(Date.today.next_occurring(:friday).to_s) + end + + it 'returns only the next expiry' do + result = contract.atm_options(sort: :expiry, limit_expirations: :monthly) + expect(result.keys).to all(be_a String) + expect(result.keys.size).to be > 1 + + current_month_third_friday = Date.today.beginning_of_month.next_occurring(:friday) + 14 + next_month_third_friday = Date.today.next_month.beginning_of_month.next_occurring(:friday) + 14 + + expect(result.keys.first).to satisfy do |key| + [current_month_third_friday.to_s, next_month_third_friday.to_s].include?(key) + end + end + it 'returns correctly OTM put options' do result = contract.otm_options expect(result.keys).to all(be_a BigDecimal) From f141859c1cba6902e266c2b93a8e321863eacf2e Mon Sep 17 00:00:00 2001 From: psmandzich Date: Fri, 3 May 2024 12:52:27 +0200 Subject: [PATCH 12/13] move option_chain_defition selection to request block --- plugins/ib/option_chain.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 7111984..7a3053f 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -46,17 +46,17 @@ def option_chain(ref_price: :request, right: :put, sort: :strike, limit_expirati @option_chain_definition << finalize.pop end ib.unsubscribe sub_sdop, sub_ocd + + @option_chain_definition.compact! + Connection.logger.info { @option_chain_definition.map { |x| x.slice(:trading_class, :exchange)} } + @option_chain_definition = @option_chain_definition.find { |x| (trading_class.blank? || x[:trading_class] == trading_class) && (exchange.blank? || x[:exchange] == exchange) } || + @option_chain_definition.find { |x| x[:exchange] == contract.exchange && x[:trading_class] == contract.trading_class } || + @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || + @option_chain_definition.first else Connection.logger.info { "#{to_human} : using cached data" } end - @option_chain_definition.compact! - Connection.logger.info { @option_chain_definition.map { |x| x.slice(:trading_class, :exchange)} } - @option_chain_definition = @option_chain_definition.find { |x| (trading_class.blank? || x[:trading_class] == trading_class) && (exchange.blank? || x[:exchange] == exchange) } || - @option_chain_definition.find { |x| x[:exchange] == contract.exchange && x[:trading_class] == contract.trading_class } || - @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || - @option_chain_definition.first - # ----------------------------------------------------------------------------------------------------- # select values and assign to options # From 95fbd5d7b3281eaa648864ea7ed5d7ebade9fefa Mon Sep 17 00:00:00 2001 From: psmandzich Date: Fri, 3 May 2024 13:40:16 +0200 Subject: [PATCH 13/13] move requesting of option_chain_definition to own method --- plugins/ib/option_chain.rb | 76 +++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/plugins/ib/option_chain.rb b/plugins/ib/option_chain.rb index 7a3053f..10e26e9 100644 --- a/plugins/ib/option_chain.rb +++ b/plugins/ib/option_chain.rb @@ -13,46 +13,10 @@ module OptionChain ### 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, limit_expirations: nil, exchange: nil, 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) do |msg| - finalize.close if msg.request_id == my_req - end - sub_ocd = ib.subscribe(:OptionChainDefinition) do |msg| - finalize.push(msg.data) if msg.request_id == my_req - end - - contract = verify.first # ensure a complete set of attributes - my_req = ib.send_message :RequestOptionChainDefinition, - con_id: contract.con_id, - symbol: contract.symbol, - exchange: contract.sec_type == :future ? contract.exchange : "", # BOX,CBOE', - trading_class:, - sec_type: contract[:sec_type] - - until finalize.closed? - @option_chain_definition << finalize.pop - end - ib.unsubscribe sub_sdop, sub_ocd - - @option_chain_definition.compact! - Connection.logger.info { @option_chain_definition.map { |x| x.slice(:trading_class, :exchange)} } - @option_chain_definition = @option_chain_definition.find { |x| (trading_class.blank? || x[:trading_class] == trading_class) && (exchange.blank? || x[:exchange] == exchange) } || - @option_chain_definition.find { |x| x[:exchange] == contract.exchange && x[:trading_class] == contract.trading_class } || - @option_chain_definition.find { |x| x[:exchange] == 'SMART' } || - @option_chain_definition.first + @option_chain_definition = request_option_chain_defintion(exchange:, trading_class:) else Connection.logger.info { "#{to_human} : using cached data" } end @@ -136,6 +100,44 @@ def otm_options(count: 5, right: :put, ref_price: :request, sort: :strike, **par private + def request_option_chain_defintion(exchange:, trading_class:) + my_req = nil + ib = Connection.current + finalize = Queue.new + option_chain_definitions = [] + + option_chain_definition_subscription = ib.subscribe(:SecurityDefinitionOptionParameterEnd) do |msg| + finalize.close if msg.request_id == my_req + end + option_chain_defintion_callback = ib.subscribe(:OptionChainDefinition) do |msg| + finalize.push(msg.data) if msg.request_id == my_req + end + + contract = verify.first # ensure a complete set of attributes + my_req = ib.send_message :RequestOptionChainDefinition, + con_id: contract.con_id, + symbol: contract.symbol, + exchange: contract.sec_type == :future ? contract.exchange : "", # BOX,CBOE', + trading_class:, + sec_type: contract[:sec_type] + + until finalize.closed? + option_chain_definitions << finalize.pop + end + ib.unsubscribe option_chain_definition_subscription, option_chain_defintion_callback + + option_chain_definitions.compact! + + option_chain = option_chain_definitions.find do |definition| + (trading_class.blank? || definition[:trading_class] == trading_class) && (exchange.blank? || definition[:exchange] == exchange) + end + option_chain ||= option_chain_definitions.find do |definition| + definition[:exchange] == contract.exchange && definition[:trading_class] == contract.trading_class + end + option_chain ||= option_chain_definitions.find { |definition| definition[:exchange] == 'SMART' } + option_chain ||= option_chain_definitions.first + end + def option_prototype(last_trading_day, strike, right) IB::Option.new( symbol:,