ib-extensions 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +9 -0
  7. data/Gemfile.lock +112 -0
  8. data/Guardfile +24 -0
  9. data/README.md +99 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +96 -0
  12. data/bin/console.yml +3 -0
  13. data/bin/gateway.rb +97 -0
  14. data/bin/setup +8 -0
  15. data/changelog.md +31 -0
  16. data/examples/cancel_orders +74 -0
  17. data/examples/eod +35 -0
  18. data/examples/input.rb +475 -0
  19. data/examples/market_price +57 -0
  20. data/examples/option_chain +67 -0
  21. data/examples/place_and_modify_order +162 -0
  22. data/examples/place_bracket_order +62 -0
  23. data/examples/place_butterfly_order +104 -0
  24. data/examples/place_combo_order +70 -0
  25. data/examples/place_limit_order +82 -0
  26. data/examples/place_the_limit_order +145 -0
  27. data/examples/volatility_research +139 -0
  28. data/examples/what_if_order +90 -0
  29. data/ib-extensions.gemspec +37 -0
  30. data/lib/ib-gateway.rb +5 -0
  31. data/lib/ib/alerts/base-alert.rb +128 -0
  32. data/lib/ib/alerts/gateway-alerts.rb +15 -0
  33. data/lib/ib/alerts/order-alerts.rb +68 -0
  34. data/lib/ib/eod.rb +152 -0
  35. data/lib/ib/extensions.rb +9 -0
  36. data/lib/ib/extensions/contract.rb +37 -0
  37. data/lib/ib/extensions/version.rb +5 -0
  38. data/lib/ib/flex.rb +150 -0
  39. data/lib/ib/gateway.rb +425 -0
  40. data/lib/ib/gateway/account-infos.rb +115 -0
  41. data/lib/ib/gateway/order-handling.rb +150 -0
  42. data/lib/ib/market-price.rb +134 -0
  43. data/lib/ib/models/account.rb +329 -0
  44. data/lib/ib/models/spread.rb +159 -0
  45. data/lib/ib/option-chain.rb +198 -0
  46. data/lib/ib/option-greeks.rb +88 -0
  47. data/lib/ib/order-prototypes.rb +110 -0
  48. data/lib/ib/order_prototypes/abstract.rb +67 -0
  49. data/lib/ib/order_prototypes/combo.rb +46 -0
  50. data/lib/ib/order_prototypes/forex.rb +40 -0
  51. data/lib/ib/order_prototypes/limit.rb +177 -0
  52. data/lib/ib/order_prototypes/market.rb +116 -0
  53. data/lib/ib/order_prototypes/pegged.rb +173 -0
  54. data/lib/ib/order_prototypes/premarket.rb +31 -0
  55. data/lib/ib/order_prototypes/stop.rb +202 -0
  56. data/lib/ib/order_prototypes/volatility.rb +39 -0
  57. data/lib/ib/spread-prototypes.rb +62 -0
  58. data/lib/ib/spread_prototypes/butterfly.rb +79 -0
  59. data/lib/ib/spread_prototypes/calendar.rb +85 -0
  60. data/lib/ib/spread_prototypes/stock-spread.rb +48 -0
  61. data/lib/ib/spread_prototypes/straddle.rb +75 -0
  62. data/lib/ib/spread_prototypes/strangle.rb +96 -0
  63. data/lib/ib/spread_prototypes/vertical.rb +84 -0
  64. data/lib/ib/verify.rb +226 -0
  65. metadata +206 -0
@@ -0,0 +1,115 @@
1
+ require 'ib/alerts/base-alert'
2
+ require 'ib/models/account'
3
+
4
+ module IB
5
+ class Alert
6
+ class << self
7
+ def alert_2101 msg
8
+ logger.error {msg.message}
9
+ @status_2101 = msg.dup
10
+ end
11
+
12
+ def status_2101 account # resets status and raises IB::TransmissionError
13
+ error account.account + ": " +@status_2101.message, :reader unless @status_2101.nil?
14
+ @status_2101 = nil # always returns nil
15
+ end
16
+ end
17
+ end
18
+ end # module
19
+
20
+ module AccountInfos
21
+
22
+ =begin
23
+ Queries the tws for Account- and PortfolioValues
24
+ The parameter can either be the account_id, the IB::Account-Object or
25
+ an Array of account_id and IB::Account-Objects.
26
+
27
+ raises an IB::TransmissionError if the account-data are not transmitted in time (1 sec)
28
+
29
+ raises an IB::Error if less then 100 items are recieved-
30
+ =end
31
+ def get_account_data *accounts, watchlists: []
32
+
33
+ logger.progname = 'Gateway#get_account_data'
34
+
35
+ @account_data_subscription ||= subscribe_account_updates
36
+
37
+ accounts = clients if accounts.empty?
38
+ logger.warn{ "No active account present. AccountData are NOT requested" } if accounts.empty?
39
+ # Account-infos have to be requested sequencially.
40
+ # subsequent (parallel) calls kill the former once on the tws-server-side
41
+ # In addition, there is no need to cancel the subscription of an request, as a new
42
+ # one overwrites the active one.
43
+ accounts.each do | ac |
44
+ account = ac.is_a?( IB::Account ) ? ac : clients.find{|x| x.account == ac }
45
+ error( "No Account detected " ) unless account.is_a? IB::Account
46
+ # don't repeat the query until 170 sec. have passed since the previous update
47
+ if account.last_updated.nil? || ( Time.now - account.last_updated ) > 170 # sec
48
+ logger.debug{ "#{account.account} :: Requesting AccountData " }
49
+ account.update_attribute :connected, false # indicates: AccountUpdate in Progress
50
+ # reset account and portfolio-values
51
+ account.portfolio_values = []
52
+ account.account_values = []
53
+ send_message :RequestAccountData, subscribe: true, account_code: account.account
54
+ Timeout::timeout(3, IB::TransmissionError, "RequestAccountData failed (#{account.account})") do
55
+ # initialize requests sequencially
56
+ loop{ sleep 0.1; break if account.connected }
57
+ end
58
+ if watchlists.present?
59
+ watchlists.each{|w| error "Watchlists must be IB::Symbols--Classes :.#{w.inspect}" unless w.is_a? IB::Symbols }
60
+ account.organize_portfolio_positions watchlists
61
+ end
62
+ send_message :RequestAccountData, subscribe: false ## do this only once
63
+ else
64
+ logger.info{ "#{account.account} :: Using stored AccountData " }
65
+ end
66
+ end
67
+ end
68
+
69
+
70
+ def all_contracts
71
+ clients.map(&:contracts).flat_map(&:itself).uniq(&:con_id)
72
+ end
73
+
74
+
75
+ private
76
+
77
+ # The subscription method should called only once per session.
78
+ # It places subscribers to AccountValue and PortfolioValue Messages, which should remain
79
+ # active through its session.
80
+ #
81
+
82
+ def subscribe_account_updates continously: true
83
+ tws.subscribe( :AccountValue, :PortfolioValue,:AccountDownloadEnd ) do | msg |
84
+ account_data( msg.account_name ) do | account | # enter mutex controlled zone
85
+ case msg
86
+ when IB::Messages::Incoming::AccountValue
87
+ account.account_values << msg.account_value
88
+ account.update_attribute :last_updated, Time.now
89
+ logger.debug { "#{account.account} :: #{msg.account_value.to_human }"}
90
+ when IB::Messages::Incoming::AccountDownloadEnd
91
+ if account.account_values.size > 10
92
+ # simply don't cancel the subscripton if continously is specified
93
+ # the connected flag is set in any case, indicating that valid data are present
94
+ send_message :RequestAccountData, subscribe: false, account_code: account.account unless continously
95
+ account.update_attribute :connected, true ## flag: Account is completely initialized
96
+ logger.info { "#{account.account} => Count of AccountValues: #{account.account_values.size}" }
97
+ else # unreasonable account_data recieved - request is still active
98
+ error "#{account.account} => Count of AccountValues too small: #{account.account_values.size}" , :reader
99
+ end
100
+ when IB::Messages::Incoming::PortfolioValue
101
+ account.contracts.update_or_create msg.contract
102
+ account.portfolio_values << msg.portfolio_value
103
+ # msg.portfolio_value.account = account
104
+ # link contract -> portfolio value
105
+ # account.contracts.find{ |x| x.con_id == msg.contract.con_id }
106
+ # .portfolio_values
107
+ # .update_or_create( msg.portfolio_value ) { :account }
108
+ logger.debug { "#{ account.account } :: #{ msg.contract.to_human }" }
109
+ end # case
110
+ end # account_data
111
+ end # subscribe
112
+ end # def
113
+
114
+
115
+ end # module
@@ -0,0 +1,150 @@
1
+
2
+ module OrderHandling
3
+ =begin
4
+ UpdateOrderDependingObject
5
+
6
+ Generic method which enables operations on the order-Object,
7
+ which is associated to OrderState-, Execution-, CommissionReport-
8
+ events fired by the tws.
9
+ The order is identified by local_id and perm_id
10
+
11
+ Everything is carried out in a mutex-synchonized environment
12
+ =end
13
+ def update_order_dependent_object order_dependent_object # :nodoc:
14
+ account_data do | a |
15
+ order = if order_dependent_object.local_id.present?
16
+ a.locate_order( :local_id => order_dependent_object.local_id)
17
+ else
18
+ a.locate_order( :perm_id => order_dependent_object.perm_id)
19
+ end
20
+ yield order if order.present?
21
+ end
22
+ end
23
+ def initialize_order_handling
24
+ tws.subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg|
25
+ logger.progname = 'Gateway#order_handling'
26
+ case msg
27
+
28
+ when IB::Messages::Incoming::CommissionReport
29
+ # Commission-Reports are not assigned to a order -
30
+ logger.info "CommissionReport -------#{msg.exec_id} :...:C: #{msg.commission} :...:P/L: #{msg.realized_pnl}-"
31
+ when IB::Messages::Incoming::OrderStatus
32
+
33
+ # The order-state only links via local_id and perm_id to orders.
34
+ # There is no reference to a contract or an account
35
+
36
+ success= update_order_dependent_object( msg.order_state) do |o|
37
+ o.order_states.update_or_create msg.order_state, :status
38
+ end
39
+
40
+ logger.info { "Order State not assigned-- #{msg.order_state.to_human} ----------" } if success.nil?
41
+
42
+ when IB::Messages::Incoming::OpenOrder
43
+ ## todo --> handling of bags --> no con_id
44
+ account_data(msg.order.account) do | this_account |
45
+ # first update the contracts
46
+ # make open order equal to IB::Spreads (include negativ con_id)
47
+ msg.contract[:con_id] = -msg.contract.combo_legs.map{|y| y.con_id}.sum if msg.contract.is_a? IB::Bag
48
+ msg.contract.orders.update_or_create msg.order, :local_id
49
+ this_account.contracts.first_or_create msg.contract, :con_id
50
+ # now save the order-record
51
+ msg.order.contract = msg.contract
52
+ this_account.orders.update_or_create msg.order, :local_id
53
+ end
54
+
55
+ # update_ib_order msg ## aus support
56
+ when IB::Messages::Incoming::OpenOrderEnd
57
+ # exitcondition=true
58
+ logger.debug { "OpenOrderEnd" }
59
+
60
+ when IB::Messages::Incoming::ExecutionData
61
+ # Excution-Data are fired independly from order-states.
62
+ # The Objects are stored at the associated order
63
+ success= update_order_dependent_object( msg.execution) do |o|
64
+ logger.progname = 'Gateway#order_handling::ExecutionData '
65
+ o.executions << msg.execution
66
+ if msg.execution.cumulative_quantity.to_i == o.total_quantity.abs
67
+ logger.info{ "#{o.account} --> #{o.contract.symbol}: Execution completed" }
68
+ o.order_states.first_or_create( IB::OrderState.new( perm_id: o.perm_id, local_id: o.local_id,
69
+
70
+ status: 'Filled' ), :status )
71
+ # update portfoliovalue
72
+ a = @accounts.detect{|x| x.account == o.account } # we are in a mutex controlled environment
73
+ pv = a.portfolio_values.detect{|y| y.contract.con_id == o.contract.con_id}
74
+ change = o.action == :sell ? -o.total_quantity : o.total_quantity
75
+ if pv.present?
76
+ pv.update_attribute :position, pv.position + change
77
+ else
78
+ a.portfolio_values << IB::PortfolioValue.new( position: change, contract: o.contract)
79
+ end
80
+ else
81
+ logger.debug{ "#{o.account} --> #{o.contract.symbol}: Execution not completed (#{msg.execution.cumulative_quantity.to_i}/#{o.total_quantity.abs})" }
82
+ end # branch
83
+ end # block
84
+
85
+ logger.error { "Execution-Record not assigned-- #{msg.execution.to_human} ----------" } if success.nil?
86
+
87
+ end # case msg.code
88
+ end # do
89
+ end # def subscribe
90
+
91
+ # Resets the order-array for each account first.
92
+ # Requests all open (eg. pending) orders from the tws
93
+ #
94
+ # Waits until the OpenOrderEnd-Message is recieved
95
+
96
+
97
+ def request_open_orders
98
+
99
+ exit_condition = false
100
+ subscription = tws.subscribe( :OpenOrderEnd ){ exit_condition = true }
101
+ account_data{| account | account.orders=[] }
102
+ send_message :RequestAllOpenOrders
103
+ Timeout::timeout(1, IB::TransmissionError,"OpenOrders not received" ) do
104
+ loop{ sleep 0.1; break if exit_condition }
105
+ end
106
+ tws.unsubscribe subscription
107
+ end
108
+
109
+ alias update_orders request_open_orders
110
+
111
+
112
+
113
+
114
+ end # module
115
+
116
+
117
+
118
+
119
+
120
+ module IB
121
+
122
+ class Order
123
+ def auto_adjust
124
+ # lambda to perform the calculation
125
+ adjust_price = ->(a,b) do
126
+ a=BigDecimal(a,5)
127
+ b=BigDecimal(b,5)
128
+ _,o =a.divmod(b)
129
+ a-o
130
+ end
131
+ # adjust_price[2.6896, 0.1].to_f => 2.6
132
+ # adjust_price[2.0896, 0.05].to_f => 2.05
133
+ # adjust_price[2.0896, 0.002].to_f => 2.088
134
+
135
+
136
+ error "No Contract provided to Auto adjust " unless contract.is_a? IB::Contract
137
+ unless contract.is_a? IB::Bag
138
+ # ensure that contract_details are present
139
+
140
+ contract.verify do |the_contract |
141
+ the_details = the_contract.contract_detail.presence || the_contract.verify.first.contract_detail
142
+ # there are two attributes to consider: limit_price and aux_price
143
+ # limit_price + aux_price may be nil or an empty string. Then ".to_f.zero?" becomes true
144
+ self.limit_price= adjust_price.call(limit_price.to_f, the_details.min_tick) unless limit_price.to_f.zero?
145
+ self.aux_price= adjust_price.call(aux_price.to_f, the_details.min_tick) unless aux_price.to_f.zero?
146
+ end
147
+ end
148
+ end
149
+ end # class Order
150
+ end # module
@@ -0,0 +1,134 @@
1
+
2
+ module CoreExtensions
3
+ module Array
4
+ module DuplicatesCounter
5
+ def count_duplicates
6
+ self.each_with_object(Hash.new(0)) { |element, counter| counter[element] += 1 }.sort_by{|k,v| -v}.to_h
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ Array.include CoreExtensions::Array::DuplicatesCounter
13
+ module IB
14
+
15
+
16
+
17
+ class Contract
18
+ # Ask for the Market-Price
19
+ #
20
+ # For valid contracts, either bid/ask or last_price and close_price are transmitted.
21
+ #
22
+ # If last_price is received, its returned.
23
+ # If not, midpoint (bid+ask/2) is used. Else the closing price will be returned.
24
+ #
25
+ # Any value (even 0.0) which is stored in IB::Contract.misc indicates that the contract is
26
+ # accepted by `place_order`.
27
+ #
28
+ # The result can be customized by a provided block.
29
+ #
30
+ # IB::Symbols::Stocks.sie.market_price{ |x| x }
31
+ # -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3}
32
+ #
33
+ #
34
+ # Raw-data are stored in the _bars_-attribute of IB::Contract
35
+ # (volatile, ie. data are not preserved when the Object is copied)
36
+ #
37
+ #Example: IB::Stock.new(symbol: :ge).market_price
38
+ # returns the current market-price
39
+ #
40
+ #Example: IB::Stock.new(symbol: :ge).market_price(thread: true).join
41
+ # assigns IB::Symbols.sie.misc with the value of the :last (or delayed_last) TickPrice-Message
42
+ # and returns this value, too
43
+ #
44
+ #Raises IB::Error
45
+ # if no Marketdata Subscription is present and delayed: false is specified
46
+ #
47
+ #
48
+ # Solutions: Catch the Error and retry with delayed: true
49
+ #
50
+ # if that fails use alternative exchanges (look to Contract.valid_exchanges)
51
+ #
52
+ def market_price delayed: true, thread: false
53
+
54
+ tws= Connection.current # get the initialized ib-ruby instance
55
+ the_id , the_price = nil, nil
56
+ tickdata = Hash.new
57
+ # define requested tick-attributes
58
+ last, close, bid, ask = [ [ :delayed_last , :last_price ] , [:delayed_close , :close_price ],
59
+ [ :delayed_bid , :bid_price ], [ :delayed_ask , :ask_price ]]
60
+ request_data_type = delayed ? :frozen_delayed : :frozen
61
+
62
+ tws.send_message :RequestMarketDataType, :market_data_type => IB::MARKET_DATA_TYPES.rassoc( request_data_type).first
63
+
64
+ #keep the method-call running until the request finished
65
+ #and cancel subscriptions to the message handler
66
+ # method returns the (running) thread
67
+ th = Thread.new do
68
+ finalize, raise_delay_alert = false, false
69
+ s_id = tws.subscribe(:TickSnapshotEnd){|x| finalize = true if x.ticker_id == the_id }
70
+
71
+ e_id = tws.subscribe(:Alert){|x| raise_delay_alert = true if x.code == 354 && x.error_id == the_id }
72
+ # TWS Error 354: Requested market data is not subscribed.
73
+ # r_id = tws.subscribe(:TickRequestParameters) {|x| } # raise_snapshot_alert = true if x.snapshot_permissions.to_i.zero? && x.ticker_id == the_id }
74
+
75
+ # subscribe to TickPrices
76
+ sub_id = tws.subscribe(:TickPrice ) do |msg| #, :TickSize, :TickGeneric, :TickOption) do |msg|
77
+ [last,close,bid,ask].each do |x|
78
+ tickdata[x] = msg.the_data[:price] if x.include?( IB::TICK_TYPES[ msg.the_data[:tick_type]])
79
+ # fast exit condition
80
+ finalize = true if tickdata.size ==4 || ( tickdata[bid].present? && tickdata[ask].present? )
81
+ end if msg.ticker_id == the_id
82
+ end
83
+ # initialize »the_id« that is used to identify the received tick messages
84
+ # by firing the market data request
85
+ the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
86
+
87
+ # todo implement config-feature to set timeout in configuration (DRY-Feature)
88
+ # Alternative zu Timeout
89
+ # Thread.new do
90
+ # i=0; loop{ i+=1; sleep 0.1; break if finalize || i > 1000 }
91
+
92
+ i=0;
93
+ loop{ i+=1; break if i > 1000 || finalize || raise_delay_alert; sleep 0.05 }
94
+ tws.unsubscribe sub_id, s_id, e_id
95
+ # reduce :close_price delayed_close to close a.s.o
96
+ if raise_delay_alert && !delayed
97
+ error "No Marketdata Subscription, use delayed data <-- #{to_human}"
98
+ # elsif raise_snapshot_alert
99
+ # error "No Snapshot Permissions, try alternative exchange <-- #{to_human}"
100
+ elsif i <= 1000
101
+ tz = -> (z){ z.map{|y| y.to_s.split('_')}.flatten.count_duplicates.max_by{|k,v| v}.first.to_sym}
102
+ data = tickdata.map{|x,y| [tz[x],y]}.to_h
103
+ valid_data = ->(d){ !(d.to_i.zero? || d.to_i == -1) }
104
+ self.bars << data # store raw data in bars
105
+ the_price = if block_given?
106
+ yield data
107
+ # yields {:bid=>0.10142e3, :ask=>0.10144e3, :last=>0.10142e3, :close=>0.10172e3}
108
+ else # behavior if no block is provided
109
+ if valid_data[data[:last]]
110
+ data[:last]
111
+ elsif valid_data[data[:bid]]
112
+ (data[:bid]+data[:ask])/2
113
+ elsif data[:close].present?
114
+ data[:close]
115
+ else
116
+ nil
117
+ end
118
+ end
119
+ self.misc = the_price if thread # store internally if in thread modus
120
+ else # i > 1000
121
+ tws.logger.info{ "#{to_human} --> No Marketdata received " }
122
+ end
123
+
124
+ end
125
+ if thread
126
+ th # return thread
127
+ else
128
+ th.join
129
+ the_price # return
130
+ end
131
+ end #
132
+
133
+ end
134
+ end
@@ -0,0 +1,329 @@
1
+ module IB
2
+
3
+ class Account
4
+
5
+
6
+ def account_data_scan search_key, search_currency=nil
7
+ if account_values.is_a? Array
8
+ if search_currency.present?
9
+ account_values.find_all{|x| x.key.match( search_key ) && x.currency == search_currency.upcase }
10
+ else
11
+ account_values.find_all{|x| x.key.match( search_key ) }
12
+ end
13
+
14
+ else # not tested!!
15
+ if search_currency.present?
16
+ account_values.where( ['key like %', search_key] ).where( currency: search_currency )
17
+ else # any currency
18
+ account_values.where( ['key like %', search_key] )
19
+ end
20
+ end
21
+ end
22
+
23
+
24
+
25
+ =begin rdoc
26
+ given any key of local_id, perm_id or order_ref
27
+ and an optional status, which can be a string or a
28
+ regexp ( status: /mitted/ matches Submitted and Presubmitted)
29
+ the last associated Orderrecord is returned.
30
+
31
+ Thus if several Orders are placed with the same order_ref, the active one is returned
32
+
33
+ (If multible keys are specified, local_id preceeds perm_id)
34
+
35
+ =end
36
+ def locate_order local_id: nil, perm_id: nil, order_ref: nil, status: /ubmitted/, con_id: nil
37
+ search_option= [ local_id.present? ? [:local_id , local_id] : nil ,
38
+ perm_id.present? ? [:perm_id, perm_id] : nil,
39
+ order_ref.present? ? [:order_ref , order_ref ] : nil ].compact.first
40
+ matched_items = if search_option.nil?
41
+ orders
42
+ else
43
+ orders.find_all{|x| x[search_option.first].to_i == search_option.last.to_i }
44
+ end
45
+ matched_items = matched_items.find_all{|x| x.contract.con_id == con_id } if con_id.present?
46
+
47
+ if status.present?
48
+ status = Regexp.new(status) unless status.is_a? Regexp
49
+ matched_items.detect{|x| x.order_state.status =~ status }
50
+ else
51
+ matched_items.last # return the last item
52
+ end
53
+ end
54
+
55
+
56
+ =begin rdoc
57
+ requires an IB::Order as parameter.
58
+
59
+ If attached, the associated IB::Contract is used to specify the tws-command
60
+
61
+ The associated Contract overtakes the specified (as parameter)
62
+
63
+ auto_adjust: Limit- and Aux-Prices are adjusted to Min-Tick
64
+
65
+ convert_size: The action-attribute (:buy :sell) is associated according the content of :total_quantity.
66
+
67
+
68
+ The parameter «order» is modified!
69
+
70
+ It can be used to modify and eventually cancel
71
+
72
+ The method raises an IB::TransmissionError if the transmitted order ist not acknowledged by the tws after
73
+ one second.
74
+
75
+ Example
76
+
77
+ j36 = IB::Stock.new symbol: 'J36', exchange: 'SGX'
78
+ order = IB::Limit.order size: 100, price: 65.5
79
+ g = IB::Gateway.current.clients.last
80
+
81
+ g.preview contract: j36, order: order
82
+ => {:init_margin=>0.10864874e6,
83
+ :maint_margin=>0.9704137e5,
84
+ :equity_with_loan=>0.97877973e6,
85
+ :commission=>0.524e1,
86
+ :commission_currency=>"USD",
87
+ :warning=>""}
88
+
89
+ the_local_id = g.place order: order
90
+ => 67 # returns local_id
91
+ order.contract # updated contract-record
92
+ => #<IB::Contract:0x00000000013c94b0 @attributes={:con_id=>95346693,
93
+ :exchange=>"SGX",
94
+ :right=>"",
95
+ :include_expired=>false}>
96
+
97
+ order.limit_price = 65 # set new price
98
+ g.modify order: order # and transmit
99
+ => 67 # returns local_id
100
+
101
+ g.locate_order( local_id: the_local_id )
102
+ => returns the assigned order-record for inspection
103
+
104
+ g.cancel order: order
105
+ # logger output: 05:17:11 Cancelling 65 New #250/ from 3000/DU167349>
106
+
107
+ =end
108
+
109
+ def place_order order:, contract: nil, auto_adjust: true, convert_size: false
110
+ # adjust the orderprice to min-tick
111
+ logger.progname = 'Account#PlaceOrder'
112
+ result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } }
113
+ #·IB::Symbols are always qualified. They carry a description-field
114
+ qualified_contract = ->(c) { c.description.present? || (c.con_id.present? && !c.con_id.to_i.zero?) ||
115
+ c.con_id <0 && c.sec_type == :bag }
116
+ order.contract = contract.verify.first if contract.present? && order.contract.nil?
117
+
118
+ ## sending of plain vanilla IB::Bags will fail using account.place, unless a (negative) con-id is provided!
119
+ # error "place order: ContractVerification failed. No con_id assigned" unless qualified_contract[order.contract]
120
+ ib = IB::Connection.current
121
+ wrong_order = nil
122
+ the_local_id = nil
123
+
124
+ ### Handle Error messages
125
+ ### Default action: display error in log and return nil
126
+ sa = ib.subscribe( :Alert ) do | msg |
127
+ if msg.error_id == the_local_id
128
+ if [ 110, # The price does not conform to the minimum price variation for this contract
129
+ 388, # Order size x is smaller than the minimum required size of yy.
130
+ ].include? msg.code
131
+ wrong_order = msg.error_id.to_i
132
+ ib.logger.error msg.message
133
+ end
134
+ end
135
+ end
136
+ order.account = account # assign the account_id to the account-field of IB::Order
137
+ if qualified_contract[order.contract]
138
+ self.orders.update_or_create order, :order_ref
139
+ order.auto_adjust # if auto_adjust /defined in lib/order_handling
140
+ end
141
+ if convert_size
142
+ order.action = order.total_quantity.to_i > 0 ? :buy : :sell
143
+ order.total_quantity = order.total_quantity.to_i.abs
144
+ end
145
+ # apply non_guarenteed and other stuff bound to the contract to order.
146
+ order.attributes.merge! order.contract.order_requirements unless order.contract.order_requirements.blank?
147
+ # con_id and exchange fully qualify a contract, no need to transmit other data
148
+ the_contract = order.contract.con_id >0 ? Contract.new( con_id: order.contract.con_id, exchange: order.contract.exchange) : nil
149
+ the_local_id = order.place the_contract # return the local_id
150
+ Timeout::timeout(1, IB::TransmissionError,"TimeOut ::Transmitted Order was not acknowledged") do
151
+ loop{ sleep(0.001); break if locate_order( local_id: the_local_id, status: nil ).present? }
152
+ end
153
+
154
+ ib.unsubscribe sa
155
+
156
+ if wrong_order.nil?
157
+ the_local_id # return_value
158
+ else
159
+ nil
160
+ end
161
+ end # place
162
+
163
+ # shortcut to enable
164
+ # account.place order: {} , contract: {}
165
+ # account.preview order: {} , contract: {}
166
+ # account.modify order: {}
167
+ alias place place_order
168
+
169
+ =begin #rdoc
170
+ Account#ModifyOrder operates in two modi:
171
+
172
+ First: The order is specified via local_id, perm_id or order_ref.
173
+ It is checked, whether the order is still modificable.
174
+ Then the Order ist provided through the block. Any modification is done there.
175
+ Important: The Block has to return the modified IB::Order
176
+
177
+ Second: The order can be provided as parameter as well. This will be used
178
+ without further checking. The block is now optional.
179
+ Important: The OrderRecord must provide a valid Contract.
180
+
181
+ The simple version does not adjust the given prices to tick-limits.
182
+ This has to be done manualy in the provided block
183
+ =end
184
+
185
+ def modify_order local_id: nil, order_ref: nil, order:nil
186
+
187
+ result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } }
188
+ logger.tap{ |l| l.progname = "Account #{account}#modify_order"}
189
+ order ||= locate_order( local_id: local_id,
190
+ status: /ubmitted/ ,
191
+ order_ref: order_ref )
192
+ if order.is_a? IB::Order
193
+ order.modify
194
+ else
195
+ error "No suitable IB::Order provided/detected. Instead: #{order.inspect}"
196
+ end
197
+ end
198
+
199
+ alias modify modify_order
200
+
201
+ # Preview
202
+ #
203
+ # Submits a "WhatIf" Order
204
+ #
205
+ # Returns the order_state.forcast
206
+ #
207
+ # The order received from the TWS is kept in account.orders
208
+ #
209
+ # Raises IB::TransmissionError if the Order could not be placed properly
210
+ #
211
+ def preview order:, contract: nil, **args_which_are_ignored
212
+ # to_do: use a copy of order instead of temporary setting order.what_if
213
+ result = ->(l){ orders.detect{|x| x.local_id == l && x.submitted? } }
214
+ order.what_if = true
215
+ the_local_id = place_order order: order, contract: contract
216
+ i=0; loop{ i= i+1; break if result[the_local_id] || i > 1000; sleep 0.01 }
217
+ raise IB::TransmissionError,"(Preview-)Order is not transmitted properly" if i >=1000
218
+ order.what_if = false # reset what_if flag
219
+ order.local_id = nil # reset local_id to enable reusage of the order-object for placing
220
+ result[the_local_id].order_state.forcast # return_value
221
+ end
222
+
223
+ # closes the contract by submitting an appropiate order
224
+ # the action- and total_amount attributes of the assigned order are overwritten.
225
+ #
226
+ # if a ratio-value (0 ..1) is specified in _order.total_quantity_ only a fraction of the position is closed.
227
+ # Other values are silently ignored
228
+ #
229
+ # if _reverse_ is specified, the opposide position is established.
230
+ # Any value in total_quantity is overwritten
231
+ #
232
+ # returns the order transmitted
233
+ #
234
+ # raises an IB::Error if no PortfolioValues have been loaded to the IB::Acoount
235
+ def close order:, contract: nil, reverse: false, **args_which_are_ignored
236
+ error "must only be called after initializing portfolio_values " if portfolio_values.blank?
237
+ contract_size = ->(c) do # note: portfolio_value.position is either positiv or negativ
238
+ if c.con_id <0 # Spread
239
+ p = portfolio_values.detect{|p| p.contract.con_id ==c.legs.first.con_id} &.position.to_i
240
+ p/ c.combo_legs.first.weight unless p.to_i.zero?
241
+ else
242
+ portfolio_values.detect{|x| x.contract.con_id == c.con_id} &.position.to_i # nil.to_i -->0
243
+ end
244
+ end
245
+
246
+ contract &.verify{|c| order.contract = c} # if contract is specified: don't touch the parameter, get a new object .
247
+ error "Cannot transmit the order – No Contract given " unless order.contract.is_a?(IB::Contract)
248
+
249
+ the_quantity = if reverse
250
+ -contract_size[order.contract] * 2
251
+ elsif order.total_quantity.abs < 1 && !order.total_quantity.zero?
252
+ -contract_size[order.contract] * order.total_quantity.abs
253
+ else
254
+ -contract_size[order.contract]
255
+ end
256
+ if the_quantity.zero?
257
+ logger.info{ "Cannot close #{order.contract.to_human} - no position detected"}
258
+ else
259
+ order.total_quantity = the_quantity
260
+ order.action = nil
261
+ order.local_id = nil # in any case, close is a new order
262
+ logger.info { "Order modified to close, reduce or revese position: #{order.to_human}" }
263
+ place order: order, convert_size: true
264
+ end
265
+ end
266
+
267
+ # just a wrapper to the Gateway-cancel-order method
268
+ def cancel order:
269
+ Gateway.current.cancel_order order
270
+ end
271
+
272
+ #returns an hash where portfolio_positions are grouped into Watchlists.
273
+ #
274
+ # Watchlist => [ contract => [ portfoliopositon] , ... ] ]
275
+ #
276
+ def organize_portfolio_positions the_watchlists
277
+ the_watchlists = [ the_watchlists ] unless the_watchlists.is_a?(Array)
278
+ self.focuses = portfolio_values.map do | pw |
279
+ z= the_watchlists.map do | w |
280
+ ref_con_id = pw.contract.con_id
281
+ watchlist_contract = w.find do |c|
282
+ c.is_a?(IB::Bag) ? c.combo_legs.map(&:con_id).include?(ref_con_id) : c.con_id == ref_con_id
283
+ end rescue nil
284
+ watchlist_contract.present? ? [w,watchlist_contract] : nil
285
+ end.compact
286
+
287
+ z.empty? ? [ IB::Symbols::Unspecified, pw.contract, pw ] : z.first << pw
288
+ 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
289
+ # group:by --> [a,b,c] .group_by {|_g,_| g} --->{ a => [a,b,c] }
290
+ # group_by+map --> removes "a" from the resulting array
291
+ end
292
+
293
+
294
+ def locate_contract con_id
295
+ contracts.detect{|x| x.con_id.to_i == con_id.to_i }
296
+ end
297
+
298
+ ## returns the contract definition of an complex portfolio-position detected in the account
299
+ def complex_position con_id
300
+ con_id = con_id.con_id if con_id.is_a?(IB::Contract)
301
+ focuses.map{|x,y| y.detect{|x,y| x.con_id.to_i== con_id.to_i} }.compact.flatten.first
302
+ end
303
+ end # class
304
+ ##
305
+ # in the console (call gateway with watchlist: [:Spreads, :BuyAndHold])
306
+ #head :001 > .active_accounts.first.focuses.to_a.to_human
307
+ #Unspecified
308
+ #<Stock: BLUE EUR SBF>
309
+ #<PortfolioValue: DU167348 Pos=720 @ 15.88;Value=11433.24;PNL=-4870.05 unrealized;<Stock: BLUE EUR SBF>
310
+ #<Stock: CSCO USD NASDAQ>
311
+ #<PortfolioValue: DU167348 Pos=44 @ 44.4;Value=1953.6;PNL=1009.8 unrealized;<Stock: CSCO USD NASDAQ>
312
+ #<Stock: DBB USD ARCA>
313
+ #<PortfolioValue: DU167348 Pos=-1 @ 16.575;Value=-16.58;PNL=1.05 unrealized;<Stock: DBB USD ARCA>
314
+ #<Stock: NEU USD NYSE>
315
+ #<PortfolioValue: DU167348 Pos=1 @ 375.617;Value=375.62;PNL=98.63 unrealized;<Stock: NEU USD NYSE>
316
+ #<Stock: WFC USD NYSE>
317
+ #<PortfolioValue: DU167348 Pos=100 @ 51.25;Value=5125.0;PNL=-171.0 unrealized;<Stock: WFC USD NYSE>
318
+ #BuyAndHold
319
+ #<Stock: CIEN USD NYSE>
320
+ #<PortfolioValue: DU167348 Pos=812 @ 29.637;Value=24065.57;PNL=4841.47 unrealized;<Stock: CIEN USD NYSE>
321
+ #<Stock: J36 USD SGX>
322
+ #<PortfolioValue: DU167348 Pos=100 @ 56.245;Value=5624.5;PNL=-830.66 unrealized;<Stock: J36 USD SGX>
323
+ #Spreads
324
+ #<Strangle Estx50(3200.0,3000.0)[Dec 2018]>
325
+ #<PortfolioValue: DU167348 Pos=-3 @ 168.933;Value=-5067.99;PNL=603.51 unrealized;<Option: ESTX50 20181221 call 3000.0 EUR>
326
+ #<PortfolioValue: DU167348 Pos=-3 @ 142.574;Value=-4277.22;PNL=-867.72 unrealized;<Option: ESTX50 20181221 put 3200.0 EUR>
327
+ # => nil
328
+ # #
329
+ end ## module