ib-extensions 1.1 → 1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -3
- data/Gemfile.lock +26 -26
- data/README.md +56 -10
- data/bin/console +10 -3
- data/bin/gateway +14 -9
- data/changelog.md +54 -0
- data/ib-extensions.gemspec +6 -4
- data/lib/ib/alerts/base-alert.rb +10 -13
- data/lib/ib/eod.rb +254 -125
- data/lib/ib/extensions/contract.rb +2 -30
- data/lib/ib/extensions/version.rb +1 -1
- data/lib/ib/extensions.rb +5 -0
- data/lib/ib/gateway/account-infos.rb +74 -47
- data/lib/ib/gateway/order-handling.rb +57 -25
- data/lib/ib/gateway.rb +45 -31
- data/lib/ib/market-price.rb +108 -97
- data/lib/ib/models/account.rb +178 -145
- data/lib/ib/models/bag.rb +19 -0
- data/lib/ib/models/contract.rb +16 -0
- data/lib/ib/models/future.rb +20 -0
- data/lib/ib/models/option.rb +20 -13
- data/lib/ib/option-chain.rb +36 -63
- data/lib/ib/option-greeks.rb +36 -33
- data/lib/ib/order_prototypes/all-in-one.rb +46 -0
- data/lib/ib/plot-poec.rb +60 -0
- data/lib/ib/probability_of_expiring.rb +109 -0
- data/lib/ib/spread-prototypes.rb +1 -0
- data/lib/ib/spread_prototypes/butterfly.rb +2 -4
- data/lib/ib/spread_prototypes/calendar.rb +25 -23
- data/lib/ib/spread_prototypes/straddle.rb +3 -3
- data/lib/ib/spread_prototypes/strangle.rb +8 -9
- data/lib/ib/spread_prototypes/vertical.rb +6 -7
- data/lib/ib/verify.rb +34 -39
- data/lib/ib-gateway.rb +12 -0
- metadata +53 -5
data/lib/ib/option-chain.rb
CHANGED
@@ -1,25 +1,27 @@
|
|
1
1
|
require 'ib/verify'
|
2
2
|
require 'ib/market-price'
|
3
3
|
module IB
|
4
|
-
# define a custom ErrorClass which can be fired if a verification fails
|
5
|
-
class VerifyError < StandardError
|
6
|
-
end
|
7
4
|
|
8
5
|
class Contract
|
9
6
|
|
10
7
|
|
11
8
|
|
12
|
-
# returns the Option Chain
|
9
|
+
# returns the Option Chain (monthly options, expiry: third friday)
|
10
|
+
# of the contract (if available)
|
11
|
+
#
|
13
12
|
#
|
14
13
|
## parameters
|
15
|
-
### right:: :call, :put, :straddle
|
16
|
-
### ref_price:: :request or a numeric value
|
14
|
+
### right:: :call, :put, :straddle ( default: :put )
|
15
|
+
### ref_price:: :request or a numeric value ( default: :request )
|
17
16
|
### sort:: :strike, :expiry
|
18
17
|
### exchange:: List of Exchanges to be queried (Blank for all available Exchanges)
|
19
|
-
|
18
|
+
### trading_class ( optional )
|
19
|
+
def option_chain ref_price: :request, right: :put, sort: :strike, exchange: '', trading_class: nil
|
20
20
|
|
21
21
|
ib = Connection.current
|
22
|
-
|
22
|
+
|
23
|
+
# binary interthread communication
|
24
|
+
finalize = Queue.new
|
23
25
|
|
24
26
|
## Enable Cashing of Definition-Matrix
|
25
27
|
@option_chain_definition ||= []
|
@@ -58,9 +60,8 @@ class Contract
|
|
58
60
|
sec_type: c[:sec_type]
|
59
61
|
|
60
62
|
finalize.pop # wait until data appeared
|
61
|
-
#i=0; loop { sleep 0.1; break if i> 1000 || finalize; i+=1 }
|
62
63
|
|
63
|
-
ib.unsubscribe sub_sdop
|
64
|
+
ib.unsubscribe sub_sdop, sub_ocd
|
64
65
|
else
|
65
66
|
Connection.logger.info { "#{to_human} : using cached data" }
|
66
67
|
end
|
@@ -69,18 +70,18 @@ class Contract
|
|
69
70
|
# select values and assign to options
|
70
71
|
#
|
71
72
|
unless @option_chain_definition.blank?
|
72
|
-
requested_strikes =
|
73
|
+
requested_strikes = if block_given?
|
73
74
|
ref_price = market_price if ref_price == :request
|
74
75
|
if ref_price.nil?
|
75
|
-
ref_price =
|
76
|
-
( @option_chain_definition[:strikes].max -
|
77
|
-
@option_chain_definition[:strikes].min ) / 2
|
76
|
+
ref_price = @option_chain_definition[:strikes].min +
|
77
|
+
( @option_chain_definition[:strikes].max -
|
78
|
+
@option_chain_definition[:strikes].min ) / 2
|
78
79
|
Connection.logger.warn { "#{to_human} :: market price not set – using midpoint of available strikes instead: #{ref_price.to_f}" }
|
79
80
|
end
|
80
81
|
atm_strike = @option_chain_definition[:strikes].min_by { |x| (x - ref_price).abs }
|
81
82
|
the_grouped_strikes = @option_chain_definition[:strikes].group_by{|e| e <=> atm_strike}
|
82
83
|
begin
|
83
|
-
the_strikes =
|
84
|
+
the_strikes = yield the_grouped_strikes
|
84
85
|
the_strikes.unshift atm_strike unless the_strikes.first == atm_strike # the first item is the atm-strike
|
85
86
|
the_strikes
|
86
87
|
rescue
|
@@ -92,34 +93,34 @@ class Contract
|
|
92
93
|
end
|
93
94
|
|
94
95
|
# third Friday of a month
|
95
|
-
monthly_expirations =
|
96
|
+
monthly_expirations = @option_chain_definition[:expirations].find_all {|y| (15..21).include? y.day }
|
96
97
|
# puts @option_chain_definition.inspect
|
97
|
-
option_prototype = -> ( ltd, strike ) do
|
98
|
-
IB::Option.new symbol: symbol,
|
99
|
-
exchange: @option_chain_definition[:exchange],
|
100
|
-
trading_class: @option_chain_definition[:trading_class],
|
101
|
-
multiplier: @option_chain_definition[:multiplier],
|
102
|
-
currency: currency,
|
103
|
-
last_trading_day: ltd,
|
104
|
-
strike: strike,
|
105
|
-
right: right
|
98
|
+
option_prototype = -> ( ltd, strike ) do
|
99
|
+
IB::Option.new( symbol: symbol,
|
100
|
+
exchange: @option_chain_definition[:exchange],
|
101
|
+
trading_class: @option_chain_definition[:trading_class],
|
102
|
+
multiplier: @option_chain_definition[:multiplier],
|
103
|
+
currency: currency,
|
104
|
+
last_trading_day: ltd,
|
105
|
+
strike: strike,
|
106
|
+
right: right).verify &.first
|
106
107
|
end
|
107
108
|
options_by_expiry = -> ( schema ) do
|
108
109
|
# Array: [ yymm -> Options] prepares for the correct conversion to a Hash
|
109
110
|
Hash[ monthly_expirations.map do | l_t_d |
|
110
|
-
[ l_t_d.strftime('%y%m').to_i , schema.map{ | strike | option_prototype[ l_t_d, strike ]}.compact ]
|
111
|
+
[ l_t_d.strftime('%y%m').to_i , schema.map { | strike | option_prototype[ l_t_d, strike ]}.compact ]
|
111
112
|
end ] # by Hash[ ]
|
112
113
|
end
|
113
114
|
options_by_strike = -> ( schema ) do
|
114
115
|
Hash[ schema.map do | strike |
|
115
|
-
[ strike , monthly_expirations.map{ | l_t_d | option_prototype[ l_t_d, strike ]}.compact ]
|
116
|
+
[ strike , monthly_expirations.map { | l_t_d | option_prototype[ l_t_d, strike ]}.compact ]
|
116
117
|
end ] # by Hash[ ]
|
117
118
|
end
|
118
119
|
|
119
120
|
if sort == :strike
|
120
|
-
options_by_strike[ requested_strikes ]
|
121
|
+
options_by_strike[ requested_strikes ]
|
121
122
|
else
|
122
|
-
options_by_expiry[ requested_strikes ]
|
123
|
+
options_by_expiry[ requested_strikes ]
|
123
124
|
end
|
124
125
|
else
|
125
126
|
Connection.logger.error "#{to_human} ::No Options available"
|
@@ -128,8 +129,8 @@ class Contract
|
|
128
129
|
end # def
|
129
130
|
|
130
131
|
# return a set of AtTheMoneyOptions
|
131
|
-
def atm_options ref_price: :request, right: :put
|
132
|
-
option_chain( right: right, ref_price: ref_price, sort: :expiry) do | chain |
|
132
|
+
def atm_options ref_price: :request, right: :put, **params
|
133
|
+
option_chain( right: right, ref_price: ref_price, sort: :expiry, **params) do | chain |
|
133
134
|
chain[0]
|
134
135
|
end
|
135
136
|
|
@@ -137,8 +138,8 @@ class Contract
|
|
137
138
|
end
|
138
139
|
|
139
140
|
# return InTheMoneyOptions
|
140
|
-
def itm_options count: 5, right: :put, ref_price: :request, sort: :strike
|
141
|
-
option_chain( right: right, ref_price: ref_price, sort: sort ) do | chain |
|
141
|
+
def itm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: ''
|
142
|
+
option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain |
|
142
143
|
if right == :put
|
143
144
|
above_market_price_strikes = chain[1][0..count-1]
|
144
145
|
else
|
@@ -148,8 +149,8 @@ class Contract
|
|
148
149
|
end # def
|
149
150
|
|
150
151
|
# return OutOfTheMoneyOptions
|
151
|
-
def otm_options count: 5, right: :put, ref_price: :request, sort: :strike
|
152
|
-
option_chain( right: right, ref_price: ref_price, sort: sort ) do | chain |
|
152
|
+
def otm_options count: 5, right: :put, ref_price: :request, sort: :strike, exchange: ''
|
153
|
+
option_chain( right: right, ref_price: ref_price, sort: sort, exchange: exchange ) do | chain |
|
153
154
|
if right == :put
|
154
155
|
# puts "Chain: #{chain}"
|
155
156
|
below_market_price_strikes = chain[-1][-count..-1].reverse
|
@@ -160,34 +161,6 @@ class Contract
|
|
160
161
|
end
|
161
162
|
|
162
163
|
|
163
|
-
def associate_ticdata
|
164
|
-
|
165
|
-
tws= IB::Connection.current # get the initialized ib-ruby instance
|
166
|
-
the_id = nil
|
167
|
-
finalize= false
|
168
|
-
# switch to delayed data
|
169
|
-
tws.send_message :RequestMarketDataType, :market_data_type => :delayed
|
170
|
-
|
171
|
-
s_id = tws.subscribe(:TickSnapshotEnd) { |msg| finalize = true if msg.ticker_id == the_id }
|
172
|
-
|
173
|
-
sub_id = tws.subscribe(:TickPrice, :TickSize, :TickGeneric, :TickOption) do |msg|
|
174
|
-
self.bars << msg.the_data if msg.ticker_id == the_id
|
175
|
-
end
|
176
|
-
|
177
|
-
# initialize »the_id« that is used to identify the received tick messages
|
178
|
-
# by firing the market data request
|
179
|
-
the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
|
180
|
-
|
181
|
-
#keep the method-call running until the request finished
|
182
|
-
#and cancel subscriptions to the message handler.
|
183
|
-
Thread.new do
|
184
|
-
i=0; loop{ i+=1; sleep 0.1; break if finalize || i > 1000 }
|
185
|
-
tws.unsubscribe sub_id
|
186
|
-
tws.unsubscribe s_id
|
187
|
-
#puts "#{symbol} data gathered"
|
188
|
-
end # method returns the (running) thread
|
189
|
-
|
190
|
-
end # def
|
191
164
|
end # class
|
192
165
|
|
193
166
|
|
data/lib/ib/option-greeks.rb
CHANGED
@@ -1,44 +1,45 @@
|
|
1
1
|
module IB
|
2
2
|
|
3
3
|
|
4
|
-
|
5
4
|
class Option
|
6
|
-
# Ask for the Greeks and implied Vola
|
7
|
-
#
|
5
|
+
# Ask for the Greeks and implied Vola
|
6
|
+
#
|
8
7
|
# The result can be customized by a provided block.
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
8
|
+
#
|
9
|
+
# IB::Symbols::Options.aapl.greeks{ |x| x }
|
10
|
+
# -> {"bid"=>0.10142e3, "ask"=>0.10144e3, "last"=>0.10142e3, "close"=>0.10172e3}
|
11
|
+
#
|
13
12
|
# Possible values for Parameter :what --> :all :model, :bid, :ask, :bidask, :last
|
14
|
-
#
|
13
|
+
#
|
15
14
|
def request_greeks delayed: true, what: :model, thread: false
|
16
15
|
|
17
|
-
tws=
|
16
|
+
tws = Connection.current # get the initialized ib-ruby instance
|
18
17
|
# define requested tick-attributes
|
19
|
-
request_data_type = IB::MARKET_DATA_TYPES.rassoc( delayed ? :frozen_delayed :
|
18
|
+
request_data_type = IB::MARKET_DATA_TYPES.rassoc( delayed ? :frozen_delayed : :frozen ).first
|
20
19
|
# possible types = [ [ :delayed_model_option , :model_option ] , [:delayed_last_option , :last_option ],
|
21
|
-
# [ :delayed_bid_option , :bid_option ], [ :delayed_ask_option , :ask_option ]
|
20
|
+
# [ :delayed_bid_option , :bid_option ], [ :delayed_ask_option , :ask_option ]]
|
22
21
|
tws.send_message :RequestMarketDataType, :market_data_type => request_data_type
|
23
22
|
tickdata = []
|
24
23
|
|
25
24
|
self.greek = OptionDetail.new if greek.nil?
|
26
|
-
|
25
|
+
greek.updated_at = Time.now
|
26
|
+
greek.option = self
|
27
|
+
queue = Queue.new
|
27
28
|
|
28
29
|
#keep the method-call running until the request finished
|
29
30
|
#and cancel subscriptions to the message handler
|
30
31
|
# method returns the (running) thread
|
31
32
|
th = Thread.new do
|
32
33
|
the_id = nil
|
33
|
-
finalize= false
|
34
34
|
# subscribe to TickPrices
|
35
|
-
|
36
|
-
|
35
|
+
s_id = tws.subscribe(:TickSnapshotEnd) { |msg| queue.push(true) if msg.ticker_id == the_id }
|
36
|
+
e_id = tws.subscribe(:Alert){|x| queue.push(false) if [200,353].include?( x.code) && x.error_id == the_id }
|
37
|
+
t_id = tws.subscribe( :TickSnapshotEnd, :TickPrice, :TickString, :TickSize, :TickGeneric, :MarketDataType, :TickRequestParameters ) {|msg| msg }
|
37
38
|
# TWS Error 200: No security definition has been found for the request
|
38
39
|
# TWS Error 354: Requested market data is not subscribed.
|
39
40
|
|
40
41
|
sub_id = tws.subscribe(:TickOption ) do |msg| #, :TickSize, :TickGeneric do |msg|
|
41
|
-
if msg.ticker_id == the_id && tickdata.is_a?(Array) # do nothing if tickdata have already gathered
|
42
|
+
if msg.ticker_id == the_id # && tickdata.is_a?(Array) # do nothing if tickdata have already gathered
|
42
43
|
case msg.type
|
43
44
|
when /ask/
|
44
45
|
greek.ask_price = msg.option_price unless msg.option_price.nil?
|
@@ -55,34 +56,36 @@ module IB
|
|
55
56
|
(bf + msg.greeks.keys).each{ |a| greek.send( a.to_s+"=", msg.send( a)) }
|
56
57
|
tickdata << msg if [ :all, :model ].include?( what )
|
57
58
|
end
|
58
|
-
|
59
|
-
|
59
|
+
# fast entry abortion ---> daiabled for now
|
60
|
+
# queue.push(true) if tickdata.is_a?(IB::Messages::Incoming::TickOption) || (tickdata.size == 2 && what== :bidask) || (tickdata.size == 4 && what == :all)
|
60
61
|
end
|
61
62
|
end # if sub_id
|
62
63
|
|
63
64
|
# initialize »the_id« that is used to identify the received tick messages
|
64
65
|
# by firing the market data request
|
65
|
-
|
66
|
+
iji = 0
|
67
|
+
loop do
|
68
|
+
the_id = tws.send_message :RequestMarketData, contract: self , snapshot: true
|
66
69
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
70
|
+
result = queue.pop
|
71
|
+
# reduce :close_price delayed_close to close a.s.o
|
72
|
+
if result == false
|
73
|
+
Connection.logger.info{ "#{to_human} --> No Marketdata received " }
|
74
|
+
else
|
75
|
+
self.misc = tickdata if thread # store internally if in thread modus
|
76
|
+
end
|
77
|
+
break if !tickdata.empty? || iji > 10
|
78
|
+
iji = iji + 1
|
79
|
+
Connection.logger.info{ "OptionGreeks::#{to_human} --> delayed processing. Trying again (#{iji}) " }
|
80
|
+
end
|
81
|
+
tws.unsubscribe sub_id, s_id, e_id, t_id
|
78
82
|
end # thread
|
79
83
|
if thread
|
80
84
|
th # return thread
|
81
85
|
else
|
82
86
|
th.join
|
83
|
-
|
87
|
+
greek
|
84
88
|
end
|
85
|
-
|
86
|
-
|
89
|
+
end
|
87
90
|
end
|
88
91
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module IB
|
2
|
+
|
3
|
+
#Combo-Orders are used for NonGuaranteed Orders only.
|
4
|
+
#»Normal« Option-Spreads are transmited by ordinary Limit-Orders
|
5
|
+
module Combo
|
6
|
+
### Basic Order Prototype: Combo with two limits
|
7
|
+
extend OrderPrototype
|
8
|
+
class << self
|
9
|
+
def defaults
|
10
|
+
## todo implement serialisation of key/tag Hash to camelCased-keyValue-List
|
11
|
+
# super.merge order_type: :limit , combo_params: { non_guaranteed: true}
|
12
|
+
# for the time being, we use the array representation
|
13
|
+
super.merge order_type: :limit , combo_params: [ ['NonGuaranteed', true] ]
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def requirements
|
18
|
+
Limit.requirements
|
19
|
+
end
|
20
|
+
|
21
|
+
def aliases
|
22
|
+
Limit.aliases
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def summary
|
27
|
+
<<-HERE
|
28
|
+
Create combination orders. It is constructed through options, stock and futures legs
|
29
|
+
(stock legs can be included if the order is routed through SmartRouting).
|
30
|
+
|
31
|
+
Although a combination/spread order is constructed of separate legs, it is executed
|
32
|
+
as a single transaction if it is routed directly to an exchange. For combination orders
|
33
|
+
that are SmartRouted, each leg may be executed separately to ensure best execution.
|
34
|
+
|
35
|
+
The »NonGuaranteed«-Flag is set to "false". A Pair of two securites should always be
|
36
|
+
routed »Guaranteed«, otherwise separate orders are prefered.
|
37
|
+
|
38
|
+
If a Bag-Order with »NonGuarateed :true« should be submitted, the Order-Type would be
|
39
|
+
REL+MKT, LMT+MKT, or REL+LMT
|
40
|
+
--------
|
41
|
+
Products: Options, Stocks, Futures
|
42
|
+
HERE
|
43
|
+
end # def
|
44
|
+
end # class
|
45
|
+
end # module combo
|
46
|
+
end # module ib
|
data/lib/ib/plot-poec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'distribution'
|
2
|
+
require 'gnuplot'
|
3
|
+
|
4
|
+
## source: ChatGpt
|
5
|
+
# Set the variables
|
6
|
+
s = 100 # current stock price
|
7
|
+
k = 110 # strike price
|
8
|
+
t = 0.5 # time to expiry (in years)
|
9
|
+
r = 0.02 # risk-free interest rate
|
10
|
+
sigma = 0.2 # implied volatility
|
11
|
+
p = 0.7 # probability
|
12
|
+
|
13
|
+
# Calculate d1 and d2
|
14
|
+
d1 = (Math.log(s/k) + (r + 0.5*sigma**2)*t) / (sigma * Math.sqrt(t))
|
15
|
+
d2 = d1 - sigma * Math.sqrt(t)
|
16
|
+
|
17
|
+
# Calculate the Z-score for the desired probability
|
18
|
+
z_score = Distribution::Normal.inv_cdf(p + (1-p)/2)
|
19
|
+
|
20
|
+
# Calculate the upper and lower bounds of the 70% probability range
|
21
|
+
upper_bound = s * Math.exp((r - 0.5*sigma**2)*t + sigma*Math.sqrt(t)*z_score)
|
22
|
+
lower_bound = s * Math.exp((r - 0.5*sigma**2)*t + sigma*Math.sqrt(t)*(-z_score))
|
23
|
+
|
24
|
+
# Create the plot
|
25
|
+
Gnuplot.open do |gp|
|
26
|
+
Gnuplot::Plot.new(gp) do |plot|
|
27
|
+
plot.title 'Probability Density Function with 70% probability range'
|
28
|
+
plot.xlabel 'Stock Price'
|
29
|
+
plot.ylabel 'Probability Density'
|
30
|
+
|
31
|
+
# Set the x-axis range
|
32
|
+
plot.xrange "[#{s*0.6}:#{s*1.4}]"
|
33
|
+
|
34
|
+
# Plot the probability density function
|
35
|
+
x = (s*0.6..s*1.4).step(0.1).to_a
|
36
|
+
y = x.map { |xi| Distribution::Normal.pdf((Math.log(xi/s) + (r - 0.5*sigma**2)*t) / (sigma * Math.sqrt(t))) / (xi*sigma*Math.sqrt(t)) }
|
37
|
+
plot.data << Gnuplot::DataSet.new([x, y]) do |ds|
|
38
|
+
ds.with = 'lines'
|
39
|
+
ds.linewidth = 2
|
40
|
+
ds.linecolor = 'blue'
|
41
|
+
end
|
42
|
+
|
43
|
+
# Plot the upper and lower bounds of the 70% probability range
|
44
|
+
plot.data << Gnuplot::DataSet.new([lower_bound, 0]) do |ds|
|
45
|
+
ds.with = 'lines'
|
46
|
+
ds.linewidth = 2
|
47
|
+
ds.linecolor = 'red'
|
48
|
+
end
|
49
|
+
plot.data << Gnuplot::DataSet.new([upper_bound, 0]) do |ds|
|
50
|
+
ds.with = 'lines'
|
51
|
+
ds.linewidth = 2
|
52
|
+
ds.linecolor = 'red'
|
53
|
+
end
|
54
|
+
plot.data << Gnuplot::DataSet.new([[lower_bound, upper_bound], [0, 0]]) do |ds|
|
55
|
+
ds.with = 'filledcurve x1=1 x2=2'
|
56
|
+
ds.fillcolor = 'red'
|
57
|
+
ds.fillstyle = 'transparent solid 0.2'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module IB
|
2
|
+
module ProbabilityOfExpiring
|
3
|
+
|
4
|
+
# Use by calling
|
5
|
+
# a = Stock.new symbol: 'A'
|
6
|
+
#
|
7
|
+
require 'prime'
|
8
|
+
require 'distribution'
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
def probability_of_assignment **args
|
13
|
+
( probability_of_expiring(**args) - 1 ).abs
|
14
|
+
end
|
15
|
+
def probability_of_expiring **args
|
16
|
+
@probability_of_expiring = calculate_probability_of_expiring(**args) if @probability_of_expiring.nil? || ! args.empty?
|
17
|
+
@probability_of_expiring
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
=begin
|
22
|
+
Here are the steps to calculate the probability of expiry cone for a stock in
|
23
|
+
the next six months using the Black-Scholes model:
|
24
|
+
|
25
|
+
* Determine the current stock price and the strike price for the option you
|
26
|
+
are interested in. Let's say the current stock price is $100 and the strike
|
27
|
+
price is $110. * Determine the time to expiry. In this case, we are
|
28
|
+
interested in the next six months, so the time to expiry is 0.5 years. *
|
29
|
+
Determine the implied volatility of the stock. Implied volatility is a measure
|
30
|
+
of the expected volatility of the stock over the life of the option, and can be
|
31
|
+
estimated from the option prices in the market.
|
32
|
+
|
33
|
+
* Use the Black-Scholes formula to calculate the probability of the stock
|
34
|
+
expiring within the range of prices that make up the expiry cone. The formula
|
35
|
+
is:
|
36
|
+
|
37
|
+
P = N(d2)
|
38
|
+
|
39
|
+
Where P is the probability of the stock expiring within the expiry cone, and
|
40
|
+
N is the cumulative distribution function of the standard normal
|
41
|
+
distribution. d2 is calculated as:
|
42
|
+
|
43
|
+
d2 = (ln(S/K) + (r - 0.5 * σ^2) * T) / (σ * sqrt(T))
|
44
|
+
|
45
|
+
Where S is the current stock price, K is the strike price, r is the risk-free
|
46
|
+
interest rate, σ is the implied volatility, and T is the time to expiry.
|
47
|
+
|
48
|
+
Look up the value of N(d2) in a standard normal distribution table, or use a
|
49
|
+
calculator or spreadsheet program that can calculate cumulative distribution
|
50
|
+
functions.
|
51
|
+
|
52
|
+
The result is the probability of the stock expiring within the expiry cone.
|
53
|
+
For example, if N(d2) is 0.35, then the probability of the stock expiring
|
54
|
+
within the expiry cone is 35%.
|
55
|
+
|
56
|
+
(ChatGPT)
|
57
|
+
=end
|
58
|
+
def calculate_probability_of_expiring price: nil,
|
59
|
+
interest: 0.03,
|
60
|
+
iv: nil,
|
61
|
+
strike: nil,
|
62
|
+
expiry: nil,
|
63
|
+
ref_date: Date.today
|
64
|
+
|
65
|
+
if iv.nil? && self.respond_to?( :greek )
|
66
|
+
IB::Connection.current.logger.info "Probability_of_expiring: using current IV and Underlying-Price for calculation"
|
67
|
+
request_greeks if greek.nil?
|
68
|
+
iv = greek.implied_volatility
|
69
|
+
price = greek.under_price if price.nil?
|
70
|
+
end
|
71
|
+
error "ProbabilityOfExpiringCone needs iv as input" if iv.nil? || iv.zero?
|
72
|
+
|
73
|
+
if price.nil?
|
74
|
+
price = if self.strike.to_i.zero?
|
75
|
+
market_price
|
76
|
+
else
|
77
|
+
underlying.market_price
|
78
|
+
end
|
79
|
+
end
|
80
|
+
error "ProbabilityOfExpiringCone needs price as input" if price.to_i.zero?
|
81
|
+
|
82
|
+
|
83
|
+
strike ||= self.strike
|
84
|
+
error "ProbabilityOfExpiringCone needs strike as input" if strike.to_i.zero?
|
85
|
+
|
86
|
+
if expiry.nil?
|
87
|
+
if last_trading_day == ''
|
88
|
+
error "ProbabilityOfExpiringCone needs expiry as input"
|
89
|
+
else
|
90
|
+
expiry = last_trading_day
|
91
|
+
end
|
92
|
+
end
|
93
|
+
time_to_expiry = ( Date.parse( expiry.to_s ) - ref_date ).to_i
|
94
|
+
|
95
|
+
# # Calculate d1 and d2
|
96
|
+
d1 = (Math.log(price/strike.to_f) + (interest + 0.5*iv**2)*time_to_expiry) / (iv * Math.sqrt(time_to_expiry))
|
97
|
+
d2 = d1 - iv * Math.sqrt(time_to_expiry)
|
98
|
+
#
|
99
|
+
# # Calculate the probability of expiry cone
|
100
|
+
Distribution::Normal.cdf(d2)
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Contract
|
106
|
+
include ProbabilityOfExpiring
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
data/lib/ib/spread-prototypes.rb
CHANGED
@@ -24,6 +24,7 @@ module IB
|
|
24
24
|
|
25
25
|
def initialize_spread ref_contract = nil, **attributes
|
26
26
|
error "Initializing of Spread failed – contract is missing" unless ref_contract.is_a?(IB::Contract)
|
27
|
+
# make sure that :exchange, :symbol and :currency are present
|
27
28
|
the_contract = ref_contract.merge( **attributes ).verify.first
|
28
29
|
error "Underlying for Spread is not valid: #{ref_contract.to_human}" if the_contract.nil?
|
29
30
|
the_spread= IB::Spread.new the_contract.attributes.slice( :exchange, :symbol, :currency )
|
@@ -25,8 +25,7 @@ module IB
|
|
25
25
|
error "fabrication is based on a master option. Please specify as first argument" unless master.is_a?(IB::Option)
|
26
26
|
strike = master.strike
|
27
27
|
master.right = :put unless master.right == :call
|
28
|
-
l=
|
29
|
-
# puts "master: #{master.attributes.inspect}"
|
28
|
+
l= master.verify
|
30
29
|
if l.empty?
|
31
30
|
error "Invalid Parameters. No Contract found #{master.to_human}"
|
32
31
|
elsif l.size > 1
|
@@ -43,7 +42,7 @@ module IB
|
|
43
42
|
strikes = [front, master.strike, back]
|
44
43
|
strikes.zip([1, -2, 1]).each do |strike, ratio|
|
45
44
|
action = ratio >0 ? :buy : :sell
|
46
|
-
|
45
|
+
leg = IB::Option.new( master.attributes.merge( strike: strike )).verify.first.essential
|
47
46
|
the_spread.add_leg leg, action: action, ratio: ratio.abs
|
48
47
|
end
|
49
48
|
the_spread.description = the_description( the_spread )
|
@@ -53,7 +52,6 @@ module IB
|
|
53
52
|
|
54
53
|
def build from: , front:, back:, **options
|
55
54
|
underlying_attributes = { expiry: IB::Symbols::Futures.next_expiry, right: :put }.merge( from.attributes.slice( :symbol, :currency, :exchange, :strike )).merge( options )
|
56
|
-
puts underlying_attributes.inspect
|
57
55
|
fabricate IB::Option.new( underlying_attributes), front: front, back: back
|
58
56
|
end
|
59
57
|
|
@@ -16,16 +16,13 @@ module IB
|
|
16
16
|
def fabricate master, the_other_expiry
|
17
17
|
|
18
18
|
error "Argument must be a IB::Future or IB::Option" unless [:option, :future_option, :future ].include? master.sec_type
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
27
|
-
the_spread.description = the_description( the_spread )
|
28
|
-
end
|
19
|
+
m = master.verify.first
|
20
|
+
the_other_expiry = the_other_expiry.values.first if the_other_expiry.is_a?(Hash)
|
21
|
+
back = IB::Spread.transform_distance m.expiry, the_other_expiry
|
22
|
+
calendar = m.roll expiry: back
|
23
|
+
error "Initialisation of Legs failed" if calendar.legs.size != 2
|
24
|
+
calendar.description = the_description( calendar )
|
25
|
+
calendar # return fabricated spread
|
29
26
|
end
|
30
27
|
|
31
28
|
|
@@ -46,25 +43,30 @@ module IB
|
|
46
43
|
fields[:expiry] = from.expiry unless fields.key?(:expiry)
|
47
44
|
fields[:trading_class] = from.trading_class unless fields.key?(:trading_class) || from.trading_class.empty?
|
48
45
|
fields[:multiplier] = from.multiplier unless fields.key?(:multiplier) || from.multiplier.to_i.zero?
|
49
|
-
|
46
|
+
details = from.verify.first.contract_detail
|
50
47
|
IB::Contract.new( con_id: details.under_con_id,
|
51
|
-
currency: from.currency)
|
52
|
-
.verify!
|
53
|
-
.essential
|
48
|
+
currency: from.currency).verify.first.essential
|
54
49
|
else
|
55
50
|
from
|
56
51
|
end
|
57
52
|
kind = { :front => fields.delete(:front), :back => fields.delete(:back) }
|
58
|
-
error "Specifiaction of :front and :back expiries
|
53
|
+
error "Specifiaction of :front and :back expiries necessary, got: #{kind.inspect}" if kind.values.any?(nil)
|
59
54
|
initialize_spread( underlying ) do | the_spread |
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
kind[:back] =
|
55
|
+
leg_prototype = IB::Option.new underlying.attributes
|
56
|
+
.slice( :currency, :symbol, :exchange)
|
57
|
+
.merge(defaults)
|
58
|
+
.merge( fields )
|
59
|
+
kind[:back] = IB::Spread.transform_distance kind[:front], kind[:back]
|
65
60
|
leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future)
|
66
|
-
|
67
|
-
|
61
|
+
leg1 = leg_prototype.merge(expiry: kind[:front] ).verify.first
|
62
|
+
leg2 = leg_prototype.merge(expiry: kind[:back] ).verify.first
|
63
|
+
unless leg2.is_a? IB::Option
|
64
|
+
leg2_trading_class = ''
|
65
|
+
leg2 = leg_prototype.merge(expiry: kind[:back] ).verify.first
|
66
|
+
|
67
|
+
end
|
68
|
+
the_spread.add_leg leg1 , action: :buy
|
69
|
+
the_spread.add_leg leg2 , action: :sell
|
68
70
|
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
69
71
|
the_spread.description = the_description( the_spread )
|
70
72
|
end
|
@@ -78,7 +80,7 @@ module IB
|
|
78
80
|
|
79
81
|
def the_description spread
|
80
82
|
x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:last_trading_day )].transpose
|
81
|
-
|
83
|
+
"<Calendar #{spread.symbol} #{spread.legs.first.right}(#{spread.legs.first.strike})[#{x.map{|w,l_t_d| "#{w} :#{Date.parse(l_t_d).strftime("%b %Y")} "}.join( '|+|' )} >"
|
82
84
|
end
|
83
85
|
end # class
|
84
86
|
end # module vertical
|
@@ -43,11 +43,11 @@ module IB
|
|
43
43
|
leg_prototype = IB::Option.new from.attributes
|
44
44
|
.slice( :currency, :symbol, :exchange)
|
45
45
|
.merge(defaults)
|
46
|
-
|
46
|
+
.merge( fields )
|
47
47
|
|
48
48
|
leg_prototype.sec_type = 'FOP' if from.is_a?(IB::Future)
|
49
|
-
|
50
|
-
|
49
|
+
the_spread.add_leg leg_prototype.merge( right: :put ).verify.first
|
50
|
+
the_spread.add_leg leg_prototype.merge( right: :call ).verify.first
|
51
51
|
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
52
52
|
the_spread.description = the_description( the_spread )
|
53
53
|
end
|