ib-extensions 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +112 -0
- data/Guardfile +24 -0
- data/README.md +99 -0
- data/Rakefile +6 -0
- data/bin/console +96 -0
- data/bin/console.yml +3 -0
- data/bin/gateway.rb +97 -0
- data/bin/setup +8 -0
- data/changelog.md +31 -0
- data/examples/cancel_orders +74 -0
- data/examples/eod +35 -0
- data/examples/input.rb +475 -0
- data/examples/market_price +57 -0
- data/examples/option_chain +67 -0
- data/examples/place_and_modify_order +162 -0
- data/examples/place_bracket_order +62 -0
- data/examples/place_butterfly_order +104 -0
- data/examples/place_combo_order +70 -0
- data/examples/place_limit_order +82 -0
- data/examples/place_the_limit_order +145 -0
- data/examples/volatility_research +139 -0
- data/examples/what_if_order +90 -0
- data/ib-extensions.gemspec +37 -0
- data/lib/ib-gateway.rb +5 -0
- data/lib/ib/alerts/base-alert.rb +128 -0
- data/lib/ib/alerts/gateway-alerts.rb +15 -0
- data/lib/ib/alerts/order-alerts.rb +68 -0
- data/lib/ib/eod.rb +152 -0
- data/lib/ib/extensions.rb +9 -0
- data/lib/ib/extensions/contract.rb +37 -0
- data/lib/ib/extensions/version.rb +5 -0
- data/lib/ib/flex.rb +150 -0
- data/lib/ib/gateway.rb +425 -0
- data/lib/ib/gateway/account-infos.rb +115 -0
- data/lib/ib/gateway/order-handling.rb +150 -0
- data/lib/ib/market-price.rb +134 -0
- data/lib/ib/models/account.rb +329 -0
- data/lib/ib/models/spread.rb +159 -0
- data/lib/ib/option-chain.rb +198 -0
- data/lib/ib/option-greeks.rb +88 -0
- data/lib/ib/order-prototypes.rb +110 -0
- data/lib/ib/order_prototypes/abstract.rb +67 -0
- data/lib/ib/order_prototypes/combo.rb +46 -0
- data/lib/ib/order_prototypes/forex.rb +40 -0
- data/lib/ib/order_prototypes/limit.rb +177 -0
- data/lib/ib/order_prototypes/market.rb +116 -0
- data/lib/ib/order_prototypes/pegged.rb +173 -0
- data/lib/ib/order_prototypes/premarket.rb +31 -0
- data/lib/ib/order_prototypes/stop.rb +202 -0
- data/lib/ib/order_prototypes/volatility.rb +39 -0
- data/lib/ib/spread-prototypes.rb +62 -0
- data/lib/ib/spread_prototypes/butterfly.rb +79 -0
- data/lib/ib/spread_prototypes/calendar.rb +85 -0
- data/lib/ib/spread_prototypes/stock-spread.rb +48 -0
- data/lib/ib/spread_prototypes/straddle.rb +75 -0
- data/lib/ib/spread_prototypes/strangle.rb +96 -0
- data/lib/ib/spread_prototypes/vertical.rb +84 -0
- data/lib/ib/verify.rb +226 -0
- 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
|