ib-extensions 1.1 → 1.3
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 -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
|