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.
- 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
|