ib-api 10.33.1
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 +52 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CLAUDE.md +131 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +120 -0
- data/Guardfile +24 -0
- data/LICENSE +674 -0
- data/LLM_GUIDE.md +388 -0
- data/README.md +114 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/api.gemspec +50 -0
- data/bin/console +96 -0
- data/bin/console.yml +3 -0
- data/bin/setup +8 -0
- data/bin/simple +91 -0
- data/changelog.md +32 -0
- data/conditions/ib/execution_condition.rb +31 -0
- data/conditions/ib/margin_condition.rb +28 -0
- data/conditions/ib/order_condition.rb +29 -0
- data/conditions/ib/percent_change_condition.rb +34 -0
- data/conditions/ib/price_condition.rb +44 -0
- data/conditions/ib/time_condition.rb +42 -0
- data/conditions/ib/volume_condition.rb +36 -0
- data/lib/class_extensions.rb +167 -0
- data/lib/ib/base.rb +109 -0
- data/lib/ib/base_properties.rb +178 -0
- data/lib/ib/connection.rb +573 -0
- data/lib/ib/constants.rb +402 -0
- data/lib/ib/contract.rb +30 -0
- data/lib/ib/errors.rb +52 -0
- data/lib/ib/messages/abstract_message.rb +68 -0
- data/lib/ib/messages/incoming/abstract_message.rb +116 -0
- data/lib/ib/messages/incoming/abstract_tick.rb +25 -0
- data/lib/ib/messages/incoming/account_message.rb +26 -0
- data/lib/ib/messages/incoming/alert.rb +34 -0
- data/lib/ib/messages/incoming/contract_data.rb +105 -0
- data/lib/ib/messages/incoming/contract_message.rb +13 -0
- data/lib/ib/messages/incoming/delta_neutral_validation.rb +23 -0
- data/lib/ib/messages/incoming/execution_data.rb +50 -0
- data/lib/ib/messages/incoming/histogram_data.rb +30 -0
- data/lib/ib/messages/incoming/historical_data.rb +65 -0
- data/lib/ib/messages/incoming/historical_data_update.rb +50 -0
- data/lib/ib/messages/incoming/managed_accounts.rb +21 -0
- data/lib/ib/messages/incoming/market_depth.rb +34 -0
- data/lib/ib/messages/incoming/market_depth_l2.rb +15 -0
- data/lib/ib/messages/incoming/next_valid_id.rb +19 -0
- data/lib/ib/messages/incoming/open_order.rb +290 -0
- data/lib/ib/messages/incoming/order_status.rb +85 -0
- data/lib/ib/messages/incoming/portfolio_value.rb +47 -0
- data/lib/ib/messages/incoming/position_data.rb +21 -0
- data/lib/ib/messages/incoming/positions_multi.rb +15 -0
- data/lib/ib/messages/incoming/real_time_bar.rb +32 -0
- data/lib/ib/messages/incoming/receive_fa.rb +30 -0
- data/lib/ib/messages/incoming/scanner_data.rb +54 -0
- data/lib/ib/messages/incoming/tick_by_tick.rb +77 -0
- data/lib/ib/messages/incoming/tick_efp.rb +18 -0
- data/lib/ib/messages/incoming/tick_generic.rb +12 -0
- data/lib/ib/messages/incoming/tick_option.rb +60 -0
- data/lib/ib/messages/incoming/tick_price.rb +60 -0
- data/lib/ib/messages/incoming/tick_size.rb +55 -0
- data/lib/ib/messages/incoming/tick_string.rb +13 -0
- data/lib/ib/messages/incoming.rb +292 -0
- data/lib/ib/messages/outgoing/abstract_message.rb +84 -0
- data/lib/ib/messages/outgoing/bar_request_message.rb +247 -0
- data/lib/ib/messages/outgoing/new-place-order.rb +193 -0
- data/lib/ib/messages/outgoing/old-place-order.rb +147 -0
- data/lib/ib/messages/outgoing/place_order.rb +149 -0
- data/lib/ib/messages/outgoing/request_account_summary.rb +79 -0
- data/lib/ib/messages/outgoing/request_historical_data.rb +182 -0
- data/lib/ib/messages/outgoing/request_market_data.rb +102 -0
- data/lib/ib/messages/outgoing/request_market_depth.rb +57 -0
- data/lib/ib/messages/outgoing/request_real_time_bars.rb +48 -0
- data/lib/ib/messages/outgoing/request_scanner_subscription.rb +73 -0
- data/lib/ib/messages/outgoing/request_tick_by_tick_data.rb +21 -0
- data/lib/ib/messages/outgoing.rb +410 -0
- data/lib/ib/messages.rb +139 -0
- data/lib/ib/order_condition.rb +26 -0
- data/lib/ib/plugins.rb +27 -0
- data/lib/ib/prepare_data.rb +61 -0
- data/lib/ib/raw_message_parser.rb +99 -0
- data/lib/ib/socket.rb +83 -0
- data/lib/ib/support.rb +236 -0
- data/lib/ib/version.rb +6 -0
- data/lib/ib-api.rb +44 -0
- data/lib/server_versions.rb +145 -0
- data/lib/support/array_function.rb +28 -0
- data/lib/support/logging.rb +45 -0
- data/models/ib/account.rb +72 -0
- data/models/ib/account_value.rb +33 -0
- data/models/ib/bag.rb +55 -0
- data/models/ib/bar.rb +31 -0
- data/models/ib/combo_leg.rb +127 -0
- data/models/ib/contract.rb +411 -0
- data/models/ib/contract_detail.rb +118 -0
- data/models/ib/execution.rb +67 -0
- data/models/ib/forex.rb +12 -0
- data/models/ib/future.rb +64 -0
- data/models/ib/index.rb +14 -0
- data/models/ib/option.rb +149 -0
- data/models/ib/option_detail.rb +84 -0
- data/models/ib/order.rb +720 -0
- data/models/ib/order_state.rb +155 -0
- data/models/ib/portfolio_value.rb +86 -0
- data/models/ib/spread.rb +176 -0
- data/models/ib/stock.rb +25 -0
- data/models/ib/underlying.rb +32 -0
- data/plugins/ib/advanced-account.rb +442 -0
- data/plugins/ib/alerts/base-alert.rb +125 -0
- data/plugins/ib/alerts/gateway-alerts.rb +15 -0
- data/plugins/ib/alerts/order-alerts.rb +73 -0
- data/plugins/ib/auto-adjust.rb +0 -0
- data/plugins/ib/connection-tools.rb +122 -0
- data/plugins/ib/eod.rb +326 -0
- data/plugins/ib/greeks.rb +102 -0
- data/plugins/ib/managed-accounts.rb +274 -0
- data/plugins/ib/market-price.rb +150 -0
- data/plugins/ib/option-chain.rb +167 -0
- data/plugins/ib/order-flow.rb +157 -0
- data/plugins/ib/order-prototypes/abstract.rb +67 -0
- data/plugins/ib/order-prototypes/adaptive.rb +40 -0
- data/plugins/ib/order-prototypes/all-in-one.rb +46 -0
- data/plugins/ib/order-prototypes/combo.rb +46 -0
- data/plugins/ib/order-prototypes/forex.rb +40 -0
- data/plugins/ib/order-prototypes/limit.rb +193 -0
- data/plugins/ib/order-prototypes/market.rb +116 -0
- data/plugins/ib/order-prototypes/pegged.rb +169 -0
- data/plugins/ib/order-prototypes/premarket.rb +31 -0
- data/plugins/ib/order-prototypes/stop.rb +202 -0
- data/plugins/ib/order-prototypes/volatility.rb +39 -0
- data/plugins/ib/order-prototypes.rb +118 -0
- data/plugins/ib/probability-of-expiring.rb +109 -0
- data/plugins/ib/process-orders.rb +155 -0
- data/plugins/ib/roll.rb +86 -0
- data/plugins/ib/spread-prototypes/butterfly.rb +77 -0
- data/plugins/ib/spread-prototypes/calendar.rb +97 -0
- data/plugins/ib/spread-prototypes/stock-spread.rb +56 -0
- data/plugins/ib/spread-prototypes/straddle.rb +70 -0
- data/plugins/ib/spread-prototypes/strangle.rb +93 -0
- data/plugins/ib/spread-prototypes/vertical.rb +83 -0
- data/plugins/ib/spread-prototypes.rb +70 -0
- data/plugins/ib/symbols/abstract.rb +136 -0
- data/plugins/ib/symbols/bonds.rb +28 -0
- data/plugins/ib/symbols/cfd.rb +19 -0
- data/plugins/ib/symbols/combo.rb +46 -0
- data/plugins/ib/symbols/commodity.rb +17 -0
- data/plugins/ib/symbols/forex.rb +41 -0
- data/plugins/ib/symbols/futures.rb +127 -0
- data/plugins/ib/symbols/index.rb +43 -0
- data/plugins/ib/symbols/options.rb +99 -0
- data/plugins/ib/symbols/stocks.rb +44 -0
- data/plugins/ib/symbols/version.rb +5 -0
- data/plugins/ib/symbols.rb +118 -0
- data/plugins/ib/verify.rb +226 -0
- data/symbols/w20.yml +210 -0
- data/t.txt +20 -0
- data/update.md +71 -0
- metadata +327 -0
|
@@ -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.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.present? || last_trading_day.empty?
|
|
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
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
=begin
|
|
3
|
+
|
|
4
|
+
Plugin for a comfortable processing of orders
|
|
5
|
+
|
|
6
|
+
Public API
|
|
7
|
+
==========
|
|
8
|
+
|
|
9
|
+
Extends IB::Connection
|
|
10
|
+
|
|
11
|
+
* initialize_order_handling
|
|
12
|
+
|
|
13
|
+
subscribes to various tws-messages and keeps record of the order-state
|
|
14
|
+
|
|
15
|
+
* request_open_orders
|
|
16
|
+
|
|
17
|
+
(aliased as UpdateOrders) erases account.orders and requests open-orders from the TWS
|
|
18
|
+
and populates Account#Orders
|
|
19
|
+
|
|
20
|
+
=end
|
|
21
|
+
module ProcessOrders
|
|
22
|
+
protected
|
|
23
|
+
def initialize_order_handling
|
|
24
|
+
|
|
25
|
+
subscribe( :CommissionReport, :ExecutionData, :OrderStatus, :OpenOrder, :OpenOrderEnd, :NextValidId ) do |msg|
|
|
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.save_insert msg.order_state, :status
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
logger.warn { "Order State not assigned-- #{msg.order_state.to_human} ----------" } if success.nil?
|
|
41
|
+
|
|
42
|
+
when IB::Messages::Incoming::OpenOrder
|
|
43
|
+
account_data(msg.order.account) do | this_account |
|
|
44
|
+
# first update the contracts
|
|
45
|
+
# make open order equal to IB::Spreads (include negativ con_id)
|
|
46
|
+
msg.contract[:con_id] = -msg.contract.combo_legs.map{|y| y.con_id}.sum if msg.contract.is_a? IB::Bag
|
|
47
|
+
msg.contract.orders.save_insert msg.order, :local_id
|
|
48
|
+
this_account.contracts.save_insert msg.contract, :con_id, false
|
|
49
|
+
# now save the order-record
|
|
50
|
+
msg.order.contract = msg.contract
|
|
51
|
+
this_account.orders.save_insert msg.order, :local_id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# update_ib_order msg ## aus support
|
|
55
|
+
when IB::Messages::Incoming::OpenOrderEnd
|
|
56
|
+
# exitcondition=true
|
|
57
|
+
logger.debug { "OpenOrderEnd" }
|
|
58
|
+
|
|
59
|
+
when IB::Messages::Incoming::ExecutionData
|
|
60
|
+
# Excution-Data are fired independly from order-states.
|
|
61
|
+
# The Objects are stored at the associated order
|
|
62
|
+
success = update_order_dependent_object( msg.execution) do |o|
|
|
63
|
+
o.executions << msg.execution
|
|
64
|
+
if msg.execution.cumulative_quantity.to_i == o.total_quantity.abs
|
|
65
|
+
logger.info{ "#{o.account} --> #{o.contract.symbol}: Execution completed" }
|
|
66
|
+
o.order_states << IB::OrderState.new( perm_id: o.perm_id,
|
|
67
|
+
local_id: o.local_id,
|
|
68
|
+
status: 'Filled' )
|
|
69
|
+
# update portfoliovalue
|
|
70
|
+
a = @accounts.detect{ | x | x.account == o.account } # we are in a mutex controlled environment
|
|
71
|
+
pv = a.portfolio_values.detect{ | y | y.contract.con_id == o.contract.con_id}
|
|
72
|
+
change = o.action == :sell ? -o.total_quantity : o.total_quantity
|
|
73
|
+
if pv.present?
|
|
74
|
+
pv.update_attribute :position, pv.position + change
|
|
75
|
+
else
|
|
76
|
+
a.portfolio_values << IB::PortfolioValue.new( position: change, contract: o.contract )
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
logger.debug{ "#{o.account} --> #{o.contract.symbol}: Execution not completed (#{msg.execution.cumulative_quantity.to_i}/#{o.total_quantity.abs})" }
|
|
80
|
+
end # branch
|
|
81
|
+
end # block
|
|
82
|
+
|
|
83
|
+
logger.warn { "Execution-Record not assigned-- #{msg.execution.to_human} ----------" } if success.nil?
|
|
84
|
+
|
|
85
|
+
end # case msg.code
|
|
86
|
+
end # do
|
|
87
|
+
end # def subscribe
|
|
88
|
+
|
|
89
|
+
# Resets the order-array for each account.
|
|
90
|
+
# Requests all open (eg. pending) orders from the tws
|
|
91
|
+
#
|
|
92
|
+
# Waits until the OpenOrderEnd-Message is received
|
|
93
|
+
|
|
94
|
+
public
|
|
95
|
+
def request_open_orders
|
|
96
|
+
|
|
97
|
+
q = Queue.new
|
|
98
|
+
subscription = subscribe( :OpenOrderEnd ) { q.push(true) } # signal success
|
|
99
|
+
account_data {| account | account.orders = [] }
|
|
100
|
+
send_message :RequestAllOpenOrders
|
|
101
|
+
## the OpenOrderEnd-message usually appears after 0.1 sec.
|
|
102
|
+
## we wait for 1 sec.
|
|
103
|
+
th = Thread.new{ sleep 1 ; q.close }
|
|
104
|
+
|
|
105
|
+
q.pop # wait for OpenOrderEnd or finishing of thread
|
|
106
|
+
|
|
107
|
+
unsubscribe subscription
|
|
108
|
+
if q.closed?
|
|
109
|
+
5.times do
|
|
110
|
+
logger.fatal { "Is the API in read-only modus? No Open Order Message received! "}
|
|
111
|
+
sleep 0.2
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
Thread.kill(th)
|
|
115
|
+
q.close
|
|
116
|
+
account_data {| account | account.orders } # reset order array
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
alias update_orders request_open_orders
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
=begin
|
|
124
|
+
UpdateOrderDependingObject
|
|
125
|
+
|
|
126
|
+
Generic method which enables operations on the order-Object,
|
|
127
|
+
which is associated to OrderState-, Execution-, CommissionReport-
|
|
128
|
+
events fired by the tws.
|
|
129
|
+
The order is identified by local_id and perm_id
|
|
130
|
+
|
|
131
|
+
Everything is carried out in a mutex-synchonized environment
|
|
132
|
+
=end
|
|
133
|
+
def update_order_dependent_object order_dependent_object # :nodoc:
|
|
134
|
+
account_data do | a |
|
|
135
|
+
order = if order_dependent_object.local_id.present?
|
|
136
|
+
a.locate_order local_id: order_dependent_object.local_id
|
|
137
|
+
else
|
|
138
|
+
a.locate_order perm_id: order_dependent_object.perm_id
|
|
139
|
+
end
|
|
140
|
+
yield order if order.present?
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
end # module
|
|
146
|
+
|
|
147
|
+
class Connection
|
|
148
|
+
include ProcessOrders
|
|
149
|
+
end
|
|
150
|
+
Connection.current.activate_plugin 'managed-accounts'
|
|
151
|
+
Connection.current.initialize_managed_accounts!
|
|
152
|
+
Connection.current.initialize_order_handling!
|
|
153
|
+
|
|
154
|
+
end ## module IB
|
|
155
|
+
|
data/plugins/ib/roll.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
module RollFuture
|
|
3
|
+
# helper method to roll an existing future
|
|
4
|
+
#
|
|
5
|
+
# Argument is the expiry of the target-future or the distance
|
|
6
|
+
#
|
|
7
|
+
# > nq = IB::Symbols::Futures.nq.verify.first
|
|
8
|
+
# > t= nq.roll to: '3m'
|
|
9
|
+
# > puts t.as_table
|
|
10
|
+
# ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
11
|
+
# │ Roll NQ future from Sep 24 to Dec 24 / buy 1 <Future: NQ 20240920 USD> / sell 1 <Future: NQ 20241220 USD │
|
|
12
|
+
# ├────────┬────────┬─────────────┬──────────┬──────────┬────────────┬───────────────┬───────┬────────┬──────────┤
|
|
13
|
+
# │ │ symbol │ con_id │ exchange │ expiry │ multiplier │ trading-class │ right │ strike │ currency │
|
|
14
|
+
# ╞════════╪════════╪═════════════╪══════════╪══════════╪════════════╪═══════════════╪═══════╪════════╪══════════╡
|
|
15
|
+
# │ Spread │ NQ │ -1201481183 │ CME │ │ 20 │ │ │ │ USD │
|
|
16
|
+
# │ Future │ NQ │ 637533450 │ CME │ 20240920 │ 20 │ NQ │ │ │ USD │
|
|
17
|
+
# │ Future │ NQ │ 563947733 │ CME │ 20241220 │ 20 │ NQ │ │ │ USD │
|
|
18
|
+
# └────────┴────────┴─────────────┴──────────┴──────────┴────────────┴───────────────┴───────┴────────┴──────────┘
|
|
19
|
+
# > t= nq.roll expiry: 202412
|
|
20
|
+
# > puts t.to_human
|
|
21
|
+
# <Roll NQ future from Sep 24 to Dec 24 / buy 1 <Future: NQ 20240920 USD> / sell 1 <Future: NQ 20241220 USD>
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def roll **args
|
|
25
|
+
print_expiry = ->(f){ Date.parse(f.last_trading_day).strftime('%b %y') }
|
|
26
|
+
error "specify expiry to roll a future" if args.empty?
|
|
27
|
+
args[:to] = args[:expiry] if args[:expiry].present? && args[:expiry].to_s =~ /[mwMW]$/
|
|
28
|
+
args[:expiry]= IB::Spread.transform_distance( expiry, args.delete(:to )) if args[:to].present?
|
|
29
|
+
|
|
30
|
+
new_future = merge( **args ).verify.first
|
|
31
|
+
error "Cannot roll future; target is no IB::Contract" unless new_future.is_a? IB::Future
|
|
32
|
+
target = IB::Spread.new exchange: exchange, symbol: symbol, currency: currency,
|
|
33
|
+
description: "<Roll #{symbol} future from #{print_expiry[self]} to #{print_expiry[new_future]}"
|
|
34
|
+
target.add_leg self, action: :sell
|
|
35
|
+
target.add_leg new_future, action: :buy
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
module RollOption
|
|
41
|
+
# helper method to roll an existing short-poption
|
|
42
|
+
#
|
|
43
|
+
# Arguments are strike and expiry of the target-option.
|
|
44
|
+
#
|
|
45
|
+
# Example: r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( strike: 1900 )
|
|
46
|
+
# r.to_human
|
|
47
|
+
# => " rolling <Option: RUT 20240516 put 2000.0 SMART USD> to <Option: RUT 20240516 put 1900.0 SMART USD>"
|
|
48
|
+
# r.combo_legs.to_human
|
|
49
|
+
# => ["<ComboLeg: buy 1 con_id 684936898 at SMART>", "<ComboLeg: sell 1 con_id 684936524 at SMART>"]
|
|
50
|
+
#
|
|
51
|
+
# rolls the Option to another strike and/or expiry
|
|
52
|
+
#
|
|
53
|
+
# Same Expiry, roll down the strike
|
|
54
|
+
# `r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( strike: 1900 ) `
|
|
55
|
+
#
|
|
56
|
+
# Same Expiry, roll to the next month
|
|
57
|
+
# `r= Symbols::Options.rut.merge(strike: 2000).next_expiry.roll( expiry: '+1m' ) `
|
|
58
|
+
|
|
59
|
+
def roll **args
|
|
60
|
+
error "specify strike and expiry to roll option" if args.empty?
|
|
61
|
+
args[:to] = args[:expiry] if args[:expiry].present? && args[:expiry].to_s =~ /[mwMW]$/
|
|
62
|
+
args[:expiry]= IB::Spread.transform_distance( expiry, args.delete(:to )) if args[:to].present?
|
|
63
|
+
|
|
64
|
+
new_option = merge( ** args ).then{ | y | y.next_expiry{ y.expiry } }
|
|
65
|
+
|
|
66
|
+
myself = con_id.to_i.zero? ? self.verify.first : self
|
|
67
|
+
error "Cannot roll option; target is no IB::Contract" unless new_option.is_a? IB::Option
|
|
68
|
+
error "Cannot roll option; Option cannot be verified" unless myself.is_a? IB::Option
|
|
69
|
+
target = IB::Spread.new exchange: exchange, symbol: symbol, currency: currency
|
|
70
|
+
target.add_leg myself, action: :buy
|
|
71
|
+
target.add_leg new_option, action: :sell
|
|
72
|
+
target.description= target.description.sub(/added <Option:/, 'rolling <Option:').then{|y| y.gsub /added <Option/, 'to <Option'}
|
|
73
|
+
target
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Connection.current.activate_plugin 'verify'
|
|
78
|
+
|
|
79
|
+
class Future
|
|
80
|
+
include RollFuture
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Option
|
|
84
|
+
include RollOption
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
|
|
3
|
+
module Butterfly
|
|
4
|
+
|
|
5
|
+
extend SpreadPrototype
|
|
6
|
+
class << self
|
|
7
|
+
|
|
8
|
+
# Fabricate a Butterfly from Scratch
|
|
9
|
+
# -----------------------------------------
|
|
10
|
+
#
|
|
11
|
+
#
|
|
12
|
+
#
|
|
13
|
+
# Call with
|
|
14
|
+
# IB::Butterfly.fabricate IB::Option.new( symbol: :estx50, strike: 3000, expiry:'201901'),
|
|
15
|
+
# front: 2850, back: 3150
|
|
16
|
+
#
|
|
17
|
+
# or
|
|
18
|
+
# IB::Butterfly.build from: Symbols::Index.stoxx
|
|
19
|
+
# strike: 3000
|
|
20
|
+
# expiry: '201901', front: 2850, back: 3150
|
|
21
|
+
#
|
|
22
|
+
# where :strike defines the center of the Spread.
|
|
23
|
+
def fabricate master, front:, back:
|
|
24
|
+
|
|
25
|
+
error "fabrication is based on a master option. Please specify as first argument" unless master.is_a?(IB::Option)
|
|
26
|
+
strike = master.strike
|
|
27
|
+
master.right = :put unless master.right == :call
|
|
28
|
+
l= master.verify
|
|
29
|
+
if l.empty?
|
|
30
|
+
error "Invalid Parameters. No Contract found #{master.to_human}"
|
|
31
|
+
elsif l.size > 1
|
|
32
|
+
error "ambigous contract-specification: #{l.map(&:to_human).join(';')}"
|
|
33
|
+
available_trading_classes = l.map( &:trading_class ).uniq
|
|
34
|
+
if available_trading_classes.size >1
|
|
35
|
+
error "Refine Specification with trading_class: #{available_trading_classes.join('; ')} "
|
|
36
|
+
else
|
|
37
|
+
error "Respecify expiry, verification reveals #{l.size} contracts (only 1 is allowed)"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
initialize_spread( master ) do | the_spread |
|
|
42
|
+
strikes = [front, master.strike, back]
|
|
43
|
+
strikes.zip([1, -2, 1]).each do |strike, ratio|
|
|
44
|
+
action = ratio >0 ? :buy : :sell
|
|
45
|
+
leg = IB::Option.new( master.attributes.merge( strike: strike )).verify.first.essential
|
|
46
|
+
the_spread.add_leg leg, action: action, ratio: ratio.abs
|
|
47
|
+
end
|
|
48
|
+
the_spread.description = the_description( the_spread )
|
|
49
|
+
the_spread.symbol = master.symbol
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build from: , front:, back:, **options
|
|
54
|
+
underlying_attributes = { expiry: IB::Future.next_expiry, right: :put }.merge( from.attributes.slice( :symbol, :currency, :exchange, :strike )).merge( options )
|
|
55
|
+
fabricate IB::Option.new( underlying_attributes), front: front, back: back
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def the_description spread
|
|
59
|
+
x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:strike )].transpose
|
|
60
|
+
"<Butterfly #{spread.symbol} #{spread.legs.first.right}(#{x.map{|w,strike| "#{w} :#{strike} "}.join( '|+|' )} )[#{Date.parse(spread.legs.first.last_trading_day).strftime("%b %Y")}]>"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def defaults
|
|
64
|
+
super.merge expiry: IB::Future.next_expiry,
|
|
65
|
+
right: :put
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def requirements
|
|
70
|
+
super.merge back: "the strike of the lower bougth option",
|
|
71
|
+
front: "the strike of the upper bougth option"
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end # class
|
|
76
|
+
end # module
|
|
77
|
+
end # module ib
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
|
|
3
|
+
module Calendar
|
|
4
|
+
|
|
5
|
+
extend SpreadPrototype
|
|
6
|
+
class << self
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Fabricate a Calendar-Spread from a Master-Option
|
|
10
|
+
# -----------------------------------------
|
|
11
|
+
# If one Leg is known, the other is build through replacing the expiry
|
|
12
|
+
# The second leg is always SOLD !
|
|
13
|
+
#
|
|
14
|
+
# Call with
|
|
15
|
+
# IB::Calendar.fabricate an_option, the_other_expiry
|
|
16
|
+
def fabricate master, the_other_expiry
|
|
17
|
+
|
|
18
|
+
error "Argument must be a IB::Future or IB::Option" unless [:option, :future_option, :future ].include? master.sec_type
|
|
19
|
+
m = master.verify.first
|
|
20
|
+
error "Argument is a #{master.class}, but Verification failed" unless m.is_a? IB::Contract
|
|
21
|
+
the_other_expiry = the_other_expiry.values.first if the_other_expiry.is_a?(Hash)
|
|
22
|
+
back = IB::Spread.transform_distance m.expiry, the_other_expiry
|
|
23
|
+
the_other_contract = m.merge( expiry: back ).verify.first
|
|
24
|
+
error "Verification of second leg failed" unless the_other_contract.is_a? IB::Contract
|
|
25
|
+
target = IB::Spread.new exchange: m.exchange, symbol: m.symbol, currency: m.currency
|
|
26
|
+
target.add_leg m, action: :buy
|
|
27
|
+
target.add_leg the_other_contract, action: :sell
|
|
28
|
+
|
|
29
|
+
# calendar = m.roll expiry: back
|
|
30
|
+
error "Initialisation of Legs failed" if target.legs.size != 2
|
|
31
|
+
target.description = the_description( target )
|
|
32
|
+
target # return fabricated spread
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Build Vertical out of an Underlying
|
|
37
|
+
# -----------------------------------------
|
|
38
|
+
# Needed attributes: :strike, :expiry( front: expiry1, back: expiry2 ), right
|
|
39
|
+
#
|
|
40
|
+
# Optional: :trading_class, :multiplier
|
|
41
|
+
#
|
|
42
|
+
# Call with
|
|
43
|
+
# IB::Calendar.build from: IB::Contract, front: an_expiry, back: an_expiry,
|
|
44
|
+
# right: {put or call}, strike: a_strike
|
|
45
|
+
def build from:, front: nil, back: nil, right: :put, strike: nil, **fields
|
|
46
|
+
underlying = if from.is_a? IB::Option
|
|
47
|
+
right ||= from.right
|
|
48
|
+
front ||= from.expiry
|
|
49
|
+
strike ||= from.strike
|
|
50
|
+
details = from.verify.first.contract_detail
|
|
51
|
+
IB::Contract.new( con_id: details.under_con_id,
|
|
52
|
+
currency: from.currency).verify.first.essential
|
|
53
|
+
else
|
|
54
|
+
error "missing essential parameter: `strike`" unless strike.present?
|
|
55
|
+
from
|
|
56
|
+
end
|
|
57
|
+
error "`front:` and `back:` expiries are required" unless front.present? && back.present?
|
|
58
|
+
kind = { :front => front, :back => back }
|
|
59
|
+
initialize_spread( underlying ) do | the_spread |
|
|
60
|
+
leg_prototype = IB::Option.new underlying.invariant_attributes.except( :sec_type )
|
|
61
|
+
.slice( :currency, :symbol, :exchange )
|
|
62
|
+
.merge(defaults)
|
|
63
|
+
.merge( fields )
|
|
64
|
+
.merge( strike: strike )
|
|
65
|
+
kind[:back] = IB::Spread.transform_distance front, back
|
|
66
|
+
leg_prototype.sec_type = 'FOP' if underlying.is_a?(IB::Future)
|
|
67
|
+
leg1 = leg_prototype.merge( expiry: kind[:front] ).verify.first
|
|
68
|
+
leg2 = leg_prototype.merge( expiry: kind[:back] ).verify.first
|
|
69
|
+
unless leg2.is_a? IB::Option
|
|
70
|
+
leg2_trading_class = ''
|
|
71
|
+
leg2 = leg_prototype.merge( expiry: kind[:back] ).verify.first
|
|
72
|
+
end
|
|
73
|
+
the_spread.add_leg leg1 , action: :buy
|
|
74
|
+
the_spread.add_leg leg2 , action: :sell
|
|
75
|
+
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
|
76
|
+
the_spread.description = the_description( the_spread ) rescue nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def defaults
|
|
81
|
+
super.merge right: :put
|
|
82
|
+
# expiry: IB::Future.next_expiry,
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def the_description spread
|
|
87
|
+
x= [ spread.combo_legs.map(&:weight) , spread.legs.map( &:last_trading_day )].transpose
|
|
88
|
+
f_or_o = if spread.legs.first.is_a?(IB::Future)
|
|
89
|
+
"Future"
|
|
90
|
+
else
|
|
91
|
+
"#{spread.legs.first.right}(#{spread.legs.first.strike})"
|
|
92
|
+
end
|
|
93
|
+
"<Calendar #{spread.symbol} #{f_or_o} [#{x.map{|w,l_t_d| "#{w}:#{Date.parse(l_t_d).strftime("%b %Y")}"}.join( '|+|' )}]>"
|
|
94
|
+
end
|
|
95
|
+
end # class
|
|
96
|
+
end # module vertical
|
|
97
|
+
end # module ib
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
|
|
3
|
+
module StockSpread
|
|
4
|
+
extend SpreadPrototype
|
|
5
|
+
class << self
|
|
6
|
+
|
|
7
|
+
# Fabricate a StockSpread from Scratch
|
|
8
|
+
# -----------------------------------------
|
|
9
|
+
#
|
|
10
|
+
#
|
|
11
|
+
#
|
|
12
|
+
# Call with
|
|
13
|
+
# IB::StockSpread.fabricate 'GE','F', ratio:[1,-2]
|
|
14
|
+
#
|
|
15
|
+
# or
|
|
16
|
+
# IB::StockSpread.fabricate IB::Stock.new(symbol:'GE'), 'F', ratio:[1,-2]
|
|
17
|
+
#
|
|
18
|
+
#
|
|
19
|
+
#
|
|
20
|
+
def fabricate *underlying, ratio: [1,-1], **args
|
|
21
|
+
#
|
|
22
|
+
are_stocks = ->(l){ l.all?{|y| y.is_a? IB::Stock} }
|
|
23
|
+
legs = underlying.map do | the_stock |
|
|
24
|
+
if the_stock.is_a? IB::Stock
|
|
25
|
+
the_stock
|
|
26
|
+
else
|
|
27
|
+
IB::Stock.new symbol: the_stock
|
|
28
|
+
end.merge( **args ).verify.first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
error "only spreads with two underyings of type »IB::Stock« are supported" unless legs.size==2 && are_stocks[legs]
|
|
32
|
+
|
|
33
|
+
initialize_spread( legs.first ) do | the_spread |
|
|
34
|
+
c_l = legs.zip(ratio).map do |l,r|
|
|
35
|
+
action = r >0 ? :buy : :sell
|
|
36
|
+
the_spread.add_leg l, action: action, ratio: r.abs
|
|
37
|
+
end
|
|
38
|
+
the_spread.description = the_description( the_spread )
|
|
39
|
+
the_spread.symbol = legs.map( &:symbol ).sort.join(",") # alphabetical order
|
|
40
|
+
the_spread.combo_params = {'NonGuaranteed' => true}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def the_description spread
|
|
45
|
+
info= spread.legs.map( &:symbol ).zip(spread.combo_legs.map( &:weight ))
|
|
46
|
+
"<StockSpread #{info.map{|c| c.join(":")}.join(" , ")} (#{spread.currency} )>"
|
|
47
|
+
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# always route a order as NonGuaranteed
|
|
51
|
+
def order_requirements
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end # class
|
|
55
|
+
end # module
|
|
56
|
+
end # module ib
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module IB
|
|
2
|
+
module Straddle
|
|
3
|
+
extend SpreadPrototype
|
|
4
|
+
class << self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Fabricate a Straddle from a Master-Option
|
|
8
|
+
# -----------------------------------------
|
|
9
|
+
# If one Leg is known, the other is simply build by flipping the right
|
|
10
|
+
#
|
|
11
|
+
# Call with
|
|
12
|
+
# IB::Spread::Straddle.fabricate an_option
|
|
13
|
+
def fabricate master
|
|
14
|
+
|
|
15
|
+
flip_right = ->(the_right){ the_right == :put ? :call : :put }
|
|
16
|
+
error "Argument must be a IB::Option" unless [ :option, :futures_option ].include?( master.sec_type )
|
|
17
|
+
|
|
18
|
+
initialize_spread( master ) do | the_spread |
|
|
19
|
+
the_spread.add_leg master.essential.verify.first
|
|
20
|
+
the_spread.add_leg( master.essential.merge( right: flip_right[master.right], local_symbol: "").verify.first )
|
|
21
|
+
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
|
22
|
+
the_spread.description = the_description( the_spread )
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build Straddle out of an Underlying
|
|
27
|
+
# -----------------------------------------
|
|
28
|
+
# Needed attributes: :strike, :expiry
|
|
29
|
+
#
|
|
30
|
+
# Optional: :trading_class, :multiplier
|
|
31
|
+
#
|
|
32
|
+
# Call with
|
|
33
|
+
# IB::Spread::Straddle.build from: IB::Contract, strike: a_value, expiry: yyyymmm(dd)
|
|
34
|
+
def build from:, ** fields
|
|
35
|
+
if from.is_a? IB::Option
|
|
36
|
+
fabricate from.merge **fields
|
|
37
|
+
else
|
|
38
|
+
initialize_spread( from ) do | the_spread |
|
|
39
|
+
leg_prototype = IB::Option.new from.attributes
|
|
40
|
+
.slice( :currency, :symbol, :exchange, :expiry) # use only these fields
|
|
41
|
+
.merge(defaults) # add defaults
|
|
42
|
+
.merge( fields ) # override attributes with parameters
|
|
43
|
+
# puts leg_prototype.attributes
|
|
44
|
+
|
|
45
|
+
leg_prototype.sec_type = 'FOP' if from.is_a?( IB::Future )
|
|
46
|
+
the_spread.add_leg leg_prototype.merge( right: :put ).verify.first
|
|
47
|
+
the_spread.add_leg leg_prototype.merge( right: :call ).verify.first
|
|
48
|
+
error "Initialisation of Legs failed" if the_spread.legs.size != 2
|
|
49
|
+
the_spread.description = the_description( the_spread )
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def defaults
|
|
55
|
+
super.merge expiry: IB::Option.next_expiry
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def requirements
|
|
59
|
+
super.merge strike: "the strike of both options",
|
|
60
|
+
expiry: "Expiry expressed as »yyyymm(dd)« (String or Integer)"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def the_description spread
|
|
64
|
+
my_strike = spread.legs.first.strike
|
|
65
|
+
"<Straddle #{spread.symbol}(#{my_strike})[#{Date.parse(spread.legs.first.expiry).strftime("%b %Y")}]>"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
end # class
|
|
69
|
+
end # module combo
|
|
70
|
+
end # module ib
|