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' 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/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/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/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 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 deleted file mode 100644 index 7399dca..0000000 --- a/plugins/ib/option-chain.rb +++ /dev/null @@ -1,167 +0,0 @@ -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/option_chain.rb b/plugins/ib/option_chain.rb new file mode 100644 index 0000000..10e26e9 --- /dev/null +++ b/plugins/ib/option_chain.rb @@ -0,0 +1,188 @@ +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, limit_expirations: nil, exchange: nil, trading_class: nil) + # ----------------------------------------------------------------------------------------------------- + # get OptionChainDefinition from IB ( instantiate cashed Hash ) + if @option_chain_definition.blank? + @option_chain_definition = request_option_chain_defintion(exchange:, trading_class:) + 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 + 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 + options_by_strike(requested_strikes, requested_expiration, right) + else + options_by_expiry(requested_strikes, requested_expiration, right) + 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:, 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, **params) + option_chain(right:, ref_price:, sort:, **params) do |chain| + if right == :put + chain[1][0..count-1] # above market price + else + chain[-1][-count..-1].reverse # below market price + end + end + end + + # return OutOfTheMoneyOptions + 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 + chain[-1][-count..-1].reverse # below market price + else + chain[1][0..count-1] # above market price + end + end + end + + 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:, + 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-%d'), + 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 + + 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' + 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 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/plugins/ib/option_chain_spec.rb b/spec/plugins/ib/option_chain_spec.rb new file mode 100644 index 0000000..3f9b299 --- /dev/null +++ b/spec/plugins/ib/option_chain_spec.rb @@ -0,0 +1,348 @@ +# 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 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: 'GE', + 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')}/, + 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 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: 'GE', + last_trading_day: first_atm_expiry_date_key, + right: 'C', + exchange: be_a(String), + local_symbol: /GE\s+#{first_atm_expiry_date.strftime('%y%m%d')}/, + 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 String) + 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 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) + 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 String) + expect(result.keys.size).to eq(12) + first_otm_expiry_date_key = result.keys.first + 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_key, + right: 'P', + exchange: be_a(String), + local_symbol: /GE\s+#{first_otm_expiry_date.strftime('%y%m%d')}/, + 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 String) + expect(result.keys.size).to eq(12) + 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: 'GE', + last_trading_day: first_itm_expiry_date_key, + right: 'P', + exchange: be_a(String), + local_symbol: /GE\s+#{first_itm_expiry_date.strftime('%y%m%d')}/, + 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 + + 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 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'