ib-api 972.5 → 972.5.2
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 +4 -4
- data/Gemfile +2 -2
- data/Gemfile.lock +36 -41
- data/README.md +21 -13
- data/VERSION +1 -1
- data/api.gemspec +2 -0
- data/bin/console +15 -17
- data/bin/console.yml +1 -1
- data/lib/extensions/class-extensions.rb +9 -0
- data/lib/ib/base_properties.rb +22 -1
- data/lib/ib/connection.rb +122 -100
- data/lib/ib/constants.rb +5 -2
- data/lib/ib/errors.rb +3 -1
- data/lib/ib/messages/incoming/alert.rb +4 -4
- data/lib/ib/messages/incoming/ticks.rb +1 -1
- data/lib/ib/messages/outgoing/bar_requests.rb +2 -2
- data/lib/ib/messages/outgoing/place_order.rb +3 -3
- data/lib/ib/messages/outgoing/request_marketdata.rb +1 -1
- data/lib/ib/raw_message.rb +85 -0
- data/lib/ib/socket.rb +2 -2
- data/lib/logging.rb +9 -8
- data/lib/models/ib/contract.rb +64 -5
- data/lib/models/ib/index.rb +1 -1
- data/lib/models/ib/option_detail.rb +13 -0
- data/lib/models/ib/order.rb +23 -0
- data/lib/models/ib/portfolio_value.rb +34 -12
- data/lib/models/ib/spread.rb +33 -18
- data/lib/requires.rb +2 -1
- metadata +32 -4
- data/lib/ib/logger.rb +0 -26
data/lib/ib/connection.rb
CHANGED
@@ -35,76 +35,81 @@ module IB
|
|
35
35
|
alias next_order_id= next_local_id=
|
36
36
|
|
37
37
|
def initialize host: '127.0.0.1',
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
# redis: false, # future plans
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
# API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# A couple of locks to avoid race conditions in JRuby
|
62
|
-
@subscribe_lock = Mutex.new
|
63
|
-
@receive_lock = Mutex.new
|
64
|
-
@message_lock = Mutex.new
|
65
|
-
|
66
|
-
@connected = false
|
67
|
-
self.next_local_id = nil
|
38
|
+
port: '4002', # IB Gateway connection (default --> demo) 4001: production
|
39
|
+
#:port => '7497', # TWS connection --> demo 7496: production
|
40
|
+
connect: true, # Connect at initialization
|
41
|
+
received: true, # Keep all received messages in a @received Hash
|
42
|
+
# redis: false, # future plans
|
43
|
+
logger: nil,
|
44
|
+
client_id: rand( 1001 .. 9999 ) ,
|
45
|
+
client_version: IB::Messages::CLIENT_VERSION, # lib/ib/server_versions.rb
|
46
|
+
optional_capacities: "", # TWS-Version 974: "+PACEAPI"
|
47
|
+
#server_version: IB::Messages::SERVER_VERSION, # lib/messages.rb
|
48
|
+
**any_other_parameters_which_are_ignored
|
49
|
+
# V 974 release motes
|
50
|
+
# API messages sent at a higher rate than 50/second can now be paced by TWS at the 50/second rate instead of potentially causing a disconnection. This is now done automatically by the RTD Server API and can be done with other API technologies by invoking SetConnectOptions("+PACEAPI") prior to eConnect.
|
51
|
+
|
52
|
+
self.class.configure_logger logger
|
53
|
+
# convert parameters into instance-variables and assign them
|
54
|
+
method(__method__).parameters.each do |type, k|
|
55
|
+
next unless type == :key ## available: key , keyrest
|
56
|
+
next if k.to_s == 'logger'
|
57
|
+
v = eval(k.to_s)
|
58
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
59
|
+
end
|
68
60
|
|
69
|
-
|
70
|
-
|
71
|
-
|
61
|
+
# A couple of locks to avoid race conditions in JRuby
|
62
|
+
@subscribe_lock = Mutex.new
|
63
|
+
@receive_lock = Mutex.new
|
64
|
+
@message_lock = Mutex.new
|
72
65
|
|
73
|
-
|
74
|
-
|
75
|
-
yield self if block_given?
|
66
|
+
@connected = false
|
67
|
+
self.next_local_id = nil
|
76
68
|
|
77
|
-
|
78
|
-
self.
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
69
|
+
# TWS always sends NextValidId message at connect -subscribe save this id
|
70
|
+
self.subscribe(:NextValidId) do |msg|
|
71
|
+
self.logger.progname = "Connection#connect"
|
72
|
+
self.next_local_id = msg.local_id
|
73
|
+
self.logger.info { "Got next valid order id: #{next_local_id}." }
|
74
|
+
end
|
75
|
+
#
|
76
|
+
# this block is executed before tws-communication is established
|
77
|
+
# Its intended for globally available subscriptions of tws-messages
|
78
|
+
yield self if block_given?
|
79
|
+
|
80
|
+
if connect
|
81
|
+
disconnect if connected?
|
82
|
+
update_next_order_id
|
83
|
+
Kernel.exit if self.next_local_id.nil? # emergency exit.
|
84
|
+
# update_next_order_id should have raised an error
|
85
|
+
end
|
86
|
+
Connection.current = self
|
87
|
+
end
|
93
88
|
|
94
89
|
# read actual order_id and
|
95
90
|
# connect if not connected
|
96
91
|
def update_next_order_id
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
92
|
+
q = Queue.new
|
93
|
+
subscription = subscribe(:NextValidId){ |msg| q.push msg.local_id }
|
94
|
+
unless connected?
|
95
|
+
connect() # connect implies requesting NextValidId
|
96
|
+
else
|
97
|
+
send_message :RequestIds
|
98
|
+
end
|
99
|
+
th = Thread.new { sleep 5; q.close }
|
100
|
+
local_id = q.pop
|
101
|
+
if q.closed?
|
102
|
+
error "Could not get NextValidID", :reader
|
103
|
+
else
|
104
|
+
th.kill
|
105
|
+
end
|
106
|
+
unsubscribe subscription
|
107
|
+
local_id # return next_id
|
108
|
+
end
|
105
109
|
|
106
110
|
### Working with connection
|
107
|
-
|
111
|
+
#
|
112
|
+
### connect can be called directly. but is mostly called through update_next_order_id
|
108
113
|
def connect
|
109
114
|
logger.progname='IB::Connection#connect'
|
110
115
|
if connected?
|
@@ -123,10 +128,6 @@ module IB
|
|
123
128
|
@local_connect_time = Time.now
|
124
129
|
end
|
125
130
|
|
126
|
-
# Sending (arbitrary) client ID to identify subsequent communications.
|
127
|
-
# The client with a client_id of 0 can manage the TWS-owned open orders.
|
128
|
-
# Other clients can only manage their own open orders.
|
129
|
-
|
130
131
|
# V100 initial handshake
|
131
132
|
# Parameters borrowed from the python client
|
132
133
|
start_api = 71
|
@@ -134,14 +135,11 @@ module IB
|
|
134
135
|
# optcap = @optional_capacities.empty? ? "" : " "+ @optional_capacities
|
135
136
|
socket.send_messages start_api, version, @client_id , @optional_capacities
|
136
137
|
@connected = true
|
137
|
-
logger.
|
138
|
+
logger.fatal{ "Connected to server, version: #{@server_version}, " +
|
139
|
+
"using client-id: #{client_id},\n connection time: " +
|
138
140
|
"#{@local_connect_time} local, " +
|
139
|
-
"#{@remote_connect_time} remote."}
|
141
|
+
"#{@remote_connect_time} remote." }
|
140
142
|
|
141
|
-
# if the client_id is wrong or the port is not accessible the first read attempt fails
|
142
|
-
# get the first message and proceed if something reasonable is recieved
|
143
|
-
the_message = process_message # recieve next_order_id
|
144
|
-
error "Check Port/Client_id ", :reader if the_message == " "
|
145
143
|
start_reader
|
146
144
|
end
|
147
145
|
|
@@ -184,7 +182,7 @@ module IB
|
|
184
182
|
when what.is_a?(Symbol)
|
185
183
|
if Messages::Incoming.const_defined?(what)
|
186
184
|
[Messages::Incoming.const_get(what)]
|
187
|
-
elsif TechnicalAnalysis::Signals.const_defined?(what)
|
185
|
+
elsif defined?( TechnicalAnalysis ) && TechnicalAnalysis::Signals.const_defined?(what)
|
188
186
|
[TechnicalAnalysis::Signals.const_get?(what)]
|
189
187
|
else
|
190
188
|
error "#{what} is no IB::Messages or TechnicalAnalyis::Signals class"
|
@@ -258,6 +256,7 @@ module IB
|
|
258
256
|
#
|
259
257
|
# wait_for depends heavyly on Connection#received. If collection of messages through recieved
|
260
258
|
# is turned off, wait_for loses most of its functionality
|
259
|
+
|
261
260
|
def wait_for *args, &block
|
262
261
|
timeout = args.find { |arg| arg.is_a? Numeric } # extract timeout from args
|
263
262
|
end_time = Time.now + (timeout || 1) # default timeout 1 sec
|
@@ -282,25 +281,40 @@ module IB
|
|
282
281
|
# Process incoming messages during *poll_time* (200) msecs, nonblocking
|
283
282
|
def process_messages poll_time = 50 # in msec
|
284
283
|
time_out = Time.now + poll_time/1000.0
|
284
|
+
begin
|
285
285
|
while (time_left = time_out - Time.now) > 0
|
286
286
|
# If socket is readable, process single incoming message
|
287
|
-
|
287
|
+
if RUBY_PLATFORM.match(/cygwin|mswin|mingw|bccwin|wince|emx/)
|
288
|
+
process_message if select [socket], nil, nil, time_left
|
289
|
+
|
290
|
+
|
288
291
|
# the following checks for shutdown of TWS side; ensures we don't run in a spin loop.
|
289
292
|
# unfortunately, it raises Errors in windows environment
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
293
|
+
else
|
294
|
+
if select [socket], nil, nil, time_left
|
295
|
+
# # Peek at the message from the socket; if it's blank then the
|
296
|
+
# # server side of connection (TWS) has likely shut down.
|
297
|
+
socket_likely_shutdown = socket.recvmsg(100, Socket::MSG_PEEK)[0] == ""
|
298
|
+
#
|
299
|
+
# # We go ahead process messages regardless (a no-op if socket_likely_shutdown).
|
300
|
+
process_message
|
301
|
+
#
|
302
|
+
# # After processing, if socket has shut down we sleep for 100ms
|
303
|
+
# # to avoid spinning in a tight loop. If the server side somehow
|
304
|
+
# # comes back up (gets reconnedted), normal processing
|
305
|
+
# # (without the 100ms wait) should happen.
|
306
|
+
sleep(0.1) if socket_likely_shutdown
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
rescue Errno::ECONNRESET => e
|
311
|
+
logger.fatal e.message
|
312
|
+
if e.message =~ /Connection reset by peer/
|
313
|
+
logger.fatal "Is another client listening on the same port?"
|
314
|
+
error "try reconnecting with a different client-id", :reader
|
315
|
+
else
|
316
|
+
logger.fatal "Aborting"
|
317
|
+
Kernel.exit
|
304
318
|
end
|
305
319
|
end
|
306
320
|
end
|
@@ -374,15 +388,20 @@ module IB
|
|
374
388
|
# If you don't start reader, you should manually poll @socket for messages
|
375
389
|
# or use #process_messages(msec) API.
|
376
390
|
def start_reader
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
391
|
+
if @reader_running
|
392
|
+
@reader_thread
|
393
|
+
elsif connected?
|
394
|
+
begin
|
395
|
+
Thread.abort_on_exception = true
|
396
|
+
@reader_running = true
|
397
|
+
@reader_thread = Thread.new { process_messages while @reader_running }
|
398
|
+
rescue Errno::ECONNRESET => e
|
399
|
+
logger.fatal e.message
|
400
|
+
Kernel.exit
|
401
|
+
end
|
402
|
+
else
|
403
|
+
error "Could not start reader, not connected!", :reader, true
|
404
|
+
end
|
386
405
|
end
|
387
406
|
|
388
407
|
protected
|
@@ -408,13 +427,16 @@ module IB
|
|
408
427
|
# Create new instance of the appropriate message type,
|
409
428
|
# and have it read the message from socket.
|
410
429
|
# NB: Failure here usually means unsupported message type received
|
411
|
-
|
412
|
-
|
413
|
-
|
430
|
+
unless Messages::Incoming::Classes[msg_id]
|
431
|
+
logger.error { "Got unsupported message #{msg_id}" }
|
432
|
+
error "Something strange happened - Reader has to be restarted" , :reader, true if msg_id.to_i.zero?
|
433
|
+
else
|
434
|
+
msg = Messages::Incoming::Classes[msg_id].new(the_decoded_message)
|
435
|
+
end
|
414
436
|
|
415
437
|
# Deliver message to all registered subscribers, alert if no subscribers
|
416
438
|
# Ruby 2.0 and above: Hashes are ordered.
|
417
|
-
# Thus first declared subscribers of
|
439
|
+
# Thus first declared subscribers of a class are executed first
|
418
440
|
@subscribe_lock.synchronize do
|
419
441
|
subscribers[msg.class].each { |_, subscriber| subscriber.call(msg) }
|
420
442
|
end
|
data/lib/ib/constants.rb
CHANGED
@@ -21,7 +21,11 @@ module IB
|
|
21
21
|
'15 mins' =>:min15,
|
22
22
|
'30 mins' =>:min30,
|
23
23
|
'1 hour' =>:hour1,
|
24
|
-
'
|
24
|
+
'4 hours' =>:hour4,
|
25
|
+
'8 hours' =>:hour8,
|
26
|
+
'1 day' => :day1,
|
27
|
+
'1 week' => :week1,
|
28
|
+
'1 month' => :month1,
|
25
29
|
}.freeze
|
26
30
|
|
27
31
|
# Enumeration of data types.
|
@@ -199,7 +203,6 @@ module IB
|
|
199
203
|
'PEG MID' => :pegged_to_midpoint, # Pegged-to-Midpoint
|
200
204
|
'PEG BENCH' => :pegged_to_benchmark, # Pegged-to-Benmchmark # Vers. 102
|
201
205
|
'VWAP' => :vwap, # VWAP-Guaranted
|
202
|
-
'OCA' => :one_cancels_all, # One-Cancels-All
|
203
206
|
'VOL' => :volatility, # Volatility
|
204
207
|
'SCALE' => :scale, # Scale
|
205
208
|
'NONE' => :none, # Used to indicate no hedge in :delta_neutral_order_type
|
data/lib/ib/errors.rb
CHANGED
@@ -38,7 +38,9 @@ def error message, type=:standard, backtrace=nil
|
|
38
38
|
IB::FlexError.new message
|
39
39
|
when :reader
|
40
40
|
IB::TransmissionError.new message
|
41
|
+
when :verify
|
42
|
+
IB::VerifyError.new message
|
41
43
|
end
|
42
|
-
e.set_backtrace(
|
44
|
+
e.set_backtrace(caller) if backtrace
|
43
45
|
raise e
|
44
46
|
end
|
@@ -4,10 +4,10 @@ module IB
|
|
4
4
|
|
5
5
|
# Called Error in Java code, but in fact this type of messages also
|
6
6
|
# deliver system alerts and additional (non-error) info from TWS.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
Alert = def_message([4, 2],
|
8
|
+
[:error_id, :int],
|
9
|
+
[:code, :int],
|
10
|
+
[:message, :string])
|
11
11
|
class Alert
|
12
12
|
# Is it an Error message?
|
13
13
|
def error?
|
@@ -62,7 +62,7 @@ module IB
|
|
62
62
|
bar_size,
|
63
63
|
data_type.to_s.upcase,
|
64
64
|
@data[:use_rth] ,
|
65
|
-
"
|
65
|
+
"" # not suported realtimebars option string
|
66
66
|
]
|
67
67
|
end
|
68
68
|
end # RequestRealTimeBars
|
@@ -189,7 +189,7 @@ module IB
|
|
189
189
|
2 , # @data[:format_date], format-date is hard-coded as int_date in incoming/historicalData
|
190
190
|
contract.serialize_legs ,
|
191
191
|
@data[:keep_up_todate], # 0 / 1
|
192
|
-
'
|
192
|
+
'' # chartOptions:TagValueList - For internal use only. Use default value XYZ.
|
193
193
|
]
|
194
194
|
end
|
195
195
|
end # RequestHistoricalData
|
@@ -74,9 +74,9 @@ module IB
|
|
74
74
|
order.all_or_none || false,
|
75
75
|
order.min_quantity || "",
|
76
76
|
order.percent_offset || '',
|
77
|
-
order.etrade_only || false,
|
78
|
-
order.firm_quote_only || false,
|
79
|
-
order.nbbo_price_cap || "",
|
77
|
+
false, # was: order.etrade_only || false, desupported in TWS > 981
|
78
|
+
false, # was: order.firm_quote_only || false, desupported in TWS > 981
|
79
|
+
order.nbbo_price_cap || "", ## desupported in TWS > 981, too. maybe we have to insert a hard-coded "" here
|
80
80
|
order[:auction_strategy],
|
81
81
|
order.starting_price,
|
82
82
|
order.stock_ref_price || "",
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IB
|
4
|
+
# Convert data passed in from a TCP socket stream, and convert into
|
5
|
+
# raw messages. The messages
|
6
|
+
class RawMessageParser
|
7
|
+
HEADER_LNGTH = 4
|
8
|
+
def initialize(socket)
|
9
|
+
@socket = socket
|
10
|
+
@data = String.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def each
|
14
|
+
while true
|
15
|
+
append_new_data
|
16
|
+
|
17
|
+
next unless length_data?
|
18
|
+
next unless enough_data?
|
19
|
+
|
20
|
+
length = next_msg_length
|
21
|
+
validate_data_header(length)
|
22
|
+
|
23
|
+
raw = grab_message(length)
|
24
|
+
validate_message_footer(raw, length)
|
25
|
+
msg = parse_message(raw, length)
|
26
|
+
remove_message
|
27
|
+
yield msg
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# extract message and convert to
|
32
|
+
# an array split by null characters.
|
33
|
+
def grab_message(length)
|
34
|
+
@data.byteslice(HEADER_LNGTH, length)
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_message(raw, length)
|
38
|
+
raw.unpack1("A#{length}").split("\0")
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_message
|
42
|
+
length = next_msg_length
|
43
|
+
leftovers = @data.byteslice(length + HEADER_LNGTH..-1)
|
44
|
+
@data = if leftovers.nil?
|
45
|
+
String.new
|
46
|
+
else
|
47
|
+
leftovers
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def enough_data?
|
52
|
+
actual_lngth = next_msg_length + HEADER_LNGTH
|
53
|
+
echo 'too little data' if next_msg_length.nil?
|
54
|
+
return false if next_msg_length.nil?
|
55
|
+
@data.bytesize >= actual_lngth
|
56
|
+
end
|
57
|
+
|
58
|
+
def length_data?
|
59
|
+
@data.bytesize > HEADER_LNGTH
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_msg_length
|
63
|
+
#can't check length if first 4 bytes don't exist
|
64
|
+
length = @data.byteslice(0..3).unpack1('N')
|
65
|
+
return 0 if length.nil?
|
66
|
+
length
|
67
|
+
end
|
68
|
+
|
69
|
+
def append_new_data
|
70
|
+
@data += @socket.recv_from
|
71
|
+
end
|
72
|
+
|
73
|
+
def validate_message_footer(msg,length)
|
74
|
+
last = msg.bytesize
|
75
|
+
last_byte = msg.byteslice(last-1,last)
|
76
|
+
raise 'Could not validate last byte' if last_byte.nil?
|
77
|
+
raise "Message has an invalid last byte. expecting \0, received: #{last_byte}" if last_byte != "\0"
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_data_header(length)
|
81
|
+
return true if length <= 5000
|
82
|
+
raise 'Message is longer than sane max length'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/ib/socket.rb
CHANGED
@@ -154,7 +154,7 @@ module IB
|
|
154
154
|
def send_messages *data
|
155
155
|
self.syswrite prepare_message(data)
|
156
156
|
rescue Errno::ECONNRESET => e
|
157
|
-
Connection.logger.
|
157
|
+
Connection.logger.fatal{ "Data not accepted by IB \n
|
158
158
|
#{data.inspect} \n
|
159
159
|
Backtrace:\n "}
|
160
160
|
Connection.logger.error e.backtrace
|
@@ -172,7 +172,7 @@ module IB
|
|
172
172
|
end while buffer.size == 4096
|
173
173
|
complete_message_buffer.join('')
|
174
174
|
rescue Errno::ECONNRESET => e
|
175
|
-
Connection.logger.
|
175
|
+
Connection.logger.fatal{ "Data Buffer is not filling \n
|
176
176
|
The Buffer: #{buffer.inspect} \n
|
177
177
|
Backtrace:\n
|
178
178
|
#{e.backtrace.join("\n") } " }
|
data/lib/logging.rb
CHANGED
@@ -26,20 +26,21 @@ module Support
|
|
26
26
|
@logger = logger
|
27
27
|
end
|
28
28
|
|
29
|
-
def configure_logger(log=
|
30
|
-
|
29
|
+
def configure_logger(log= STDOUT)
|
30
|
+
if log.is_a? Logger
|
31
31
|
@logger = log
|
32
32
|
else
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
@logger = Logger.new log
|
34
|
+
end
|
35
|
+
@logger.level = Logger::INFO
|
36
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
36
37
|
# "#{datetime.strftime("%d.%m.(%X)")}#{"%5s" % severity}->#{msg}\n"
|
37
|
-
"#{"%
|
38
|
+
"#{"%1s" % severity[0]}: #{msg}\n"
|
38
39
|
end
|
39
|
-
|
40
|
-
end # branch
|
40
|
+
@logger.debug "------------------------------ start logging ----------------------------"
|
41
41
|
end # def
|
42
42
|
end # module ClassMethods
|
43
43
|
end # module Logging
|
44
44
|
end # module Support
|
45
45
|
|
46
|
+
# source: https://github.com/jondot/sneakers/blob/master/lib/sneakers/concerns/logging.rb
|
data/lib/models/ib/contract.rb
CHANGED
@@ -5,7 +5,7 @@ require 'models/ib/underlying'
|
|
5
5
|
|
6
6
|
module IB
|
7
7
|
|
8
|
-
if defined?(Contract)
|
8
|
+
if defined?(Contract)
|
9
9
|
#Connection.current.logger.warn "Contract already a #{defined?(Contract)}"
|
10
10
|
|
11
11
|
# puts Contract.ancestors
|
@@ -235,10 +235,46 @@ module IB
|
|
235
235
|
|
236
236
|
# creates a new Contract substituting attributes by the provided key-value pairs.
|
237
237
|
#
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
238
|
+
# for convenience
|
239
|
+
# con_id, local_symbol and last_trading_day are resetted,
|
240
|
+
# the link to contract-details is savaged
|
241
|
+
#
|
242
|
+
# Example
|
243
|
+
# ge = Stock.new( symbol: :ge).verify.first
|
244
|
+
# f = ge.merge symbol: :f
|
245
|
+
#
|
246
|
+
# c = Contract.new( con_id: 428520002, exchange: 'Globex')
|
247
|
+
#puts c.verify.as_table
|
248
|
+
#┌────────┬────────┬───────────┬──────────┬──────────┬────────────┬───────────────┬───────┬────────┬──────────┐
|
249
|
+
#│ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │
|
250
|
+
#╞════════╪════════╪═══════════╪══════════╪══════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡
|
251
|
+
#│ Future │ NQ │ 428520002 │ GLOBEX │ 20210917 │ 20 │ NQ │ │ │ USD │
|
252
|
+
#└────────┴────────┴───────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘
|
253
|
+
# d= c.merge symbol: :es, trading_class: '', multiplier: 50
|
254
|
+
# puts d.verify.as_table
|
255
|
+
#┌────────┬────────┬───────────┬──────────┬──────────┬────────────┬───────────────┬───────┬────────┬──────────┐
|
256
|
+
#│ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │
|
257
|
+
#╞════════╪════════╪═══════════╪══════════╪══════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡
|
258
|
+
#│ Future │ ES │ 428520022 │ GLOBEX │ 20210917 │ 50 │ ES │ │ │ USD │
|
259
|
+
#│ Future │ ES │ 446091461 │ GLOBEX │ 20211217 │ 50 │ ES │ │ │ USD │
|
260
|
+
#│ Future │ ES │ 461318816 │ GLOBEX │ 20220318 │ 50 │ ES │ │ │ USD │
|
261
|
+
#│ Future │ ES │ 477836957 │ GLOBEX │ 20220617 │ 50 │ ES │ │ │ USD │
|
262
|
+
#│ Future │ ES │ 495512551 │ GLOBEX │ 20221216 │ 50 │ ES │ │ │ USD │
|
263
|
+
#│ Future │ ES │ 495512552 │ GLOBEX │ 20231215 │ 50 │ ES │ │ │ USD │
|
264
|
+
#│ Future │ ES │ 495512557 │ GLOBEX │ 20241220 │ 50 │ ES │ │ │ USD │
|
265
|
+
#│ Future │ ES │ 495512563 │ GLOBEX │ 20251219 │ 50 │ ES │ │ │ USD │
|
266
|
+
#│ Future │ ES │ 495512566 │ GLOBEX │ 20220916 │ 50 │ ES │ │ │ USD │
|
267
|
+
#│ Future │ ES │ 495512569 │ GLOBEX │ 20230616 │ 50 │ ES │ │ │ USD │
|
268
|
+
#│ Future │ ES │ 495512572 │ GLOBEX │ 20230317 │ 50 │ ES │ │ │ USD │
|
269
|
+
#│ Future │ ES │ 497222760 │ GLOBEX │ 20230915 │ 50 │ ES │ │ │ USD │
|
270
|
+
#└────────┴────────┴───────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘
|
271
|
+
|
272
|
+
def merge **new_attributes
|
273
|
+
|
274
|
+
resetted_attributes = [:con_id, :local_symbol, :contract_detail]
|
275
|
+
## last_trading_day / expiry needs special treatment
|
276
|
+
resetted_attributes << :last_trading_day if new_attributes.keys.include? :expiry
|
277
|
+
self.class.new attributes.reject{|k,_| resetted_attributes.include? k}.merge(new_attributes)
|
242
278
|
end
|
243
279
|
|
244
280
|
# Contract comparison
|
@@ -376,6 +412,29 @@ In places where these terms are used to indicate a concept, we have left them as
|
|
376
412
|
Hash.new
|
377
413
|
end
|
378
414
|
|
415
|
+
|
416
|
+
def table_header( &b )
|
417
|
+
if block_given?
|
418
|
+
[ yield(self) , 'symbol', 'con_id', 'exchange', 'expiry','multiplier', 'trading-class' , 'right', 'strike', 'currency' ]
|
419
|
+
else
|
420
|
+
[ '', 'symbol', 'con_id', 'exchange', 'expiry','multiplier', 'trading-class' , 'right', 'strike', 'currency' ]
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def table_row
|
425
|
+
[ self.class.to_s.demodulize, symbol,
|
426
|
+
{ value: con_id.zero? ? '' : con_id , alignment: :right},
|
427
|
+
{ value: exchange, alignment: :center},
|
428
|
+
expiry,
|
429
|
+
{ value: multiplier.zero?? "" : multiplier, alignment: :center},
|
430
|
+
{ value: trading_class, alignment: :center},
|
431
|
+
{value: right == :none ? "": right, alignment: :center },
|
432
|
+
{ value: strike.zero? ? "": strike, alignment: :right},
|
433
|
+
{ value: currency, alignment: :center} ]
|
434
|
+
|
435
|
+
end
|
436
|
+
|
437
|
+
|
379
438
|
end # class Contract
|
380
439
|
|
381
440
|
|
data/lib/models/ib/index.rb
CHANGED
@@ -4,7 +4,7 @@ module IB
|
|
4
4
|
validates_format_of :sec_type, :with => /\Aind\z/,
|
5
5
|
:message => "should be a Index"
|
6
6
|
def default_attributes
|
7
|
-
super.merge :sec_type =>
|
7
|
+
super.merge :sec_type => 'IND'
|
8
8
|
end
|
9
9
|
def to_human
|
10
10
|
"<Index: " + [symbol, currency].join(" ") + " (#{description}) >"
|
@@ -67,5 +67,18 @@ module IB
|
|
67
67
|
|
68
68
|
end
|
69
69
|
|
70
|
+
def table_header
|
71
|
+
[ 'Greeks', 'price', 'impl. vola', 'dividend', 'delta','gamma', 'vega' , 'theta']
|
72
|
+
end
|
73
|
+
|
74
|
+
def table_row
|
75
|
+
outstr= ->( item ) { { value: item.nil? ? "--" : sprintf("%g" , item) , alignment: :right } }
|
76
|
+
outprice= ->( item ) { { value: item.nil? ? "--" : sprintf("%7.2f" , item) , alignment: :right } }
|
77
|
+
option_short = ->{"#{option.right} #{option.symbol}#{ "/"+ option.trading_class unless option.trading_class == option.symbol } #{option.expiry} #{option.strike}"}
|
78
|
+
[ option_short[], outprice[ option_price ], outprice[ implied_volatility ],
|
79
|
+
outprice[ pv_dividend ],
|
80
|
+
outprice[ delta ], outprice[ gamma ], outprice[ vega ] , outprice[ theta ] ]
|
81
|
+
end
|
82
|
+
|
70
83
|
end # class
|
71
84
|
end # module
|