ib-extensions 1.0

Sign up to get free protection for your applications and to get access to all the features.
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