-
Notifications
You must be signed in to change notification settings - Fork 10
/
ib.cr
272 lines (231 loc) · 7.69 KB
/
ib.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
require "json"
require "http"
class IB
alias Float = Float64; alias Int = Int32
enum Right; Put; Call end
enum MarketDataType; Realtime; Frozen; Delayed; DelayedFrozen end
def initialize(@base_url = "http://localhost:8001"); end
def stock_contract(
symbol : String, # MSFT
exchange : String, # SMART
currency : String, # USD
) : StockContract
StockContract.from_json http_get "/api/v1/stock_contract",
{ symbol: symbol, exchange: exchange, currency: currency }
end
record StockContract,
symbol : String,
name : String,
exchange : String,
primary_exchange : String,
currency : String,
id : Int,
do
include JSON::Serializable
end
# Get all stock contracts on all exchanges
def stock_contracts(
symbol : String # MSFT
) : Array(StockContract)
Array(StockContract).from_json http_get "/api/v1/stock_contracts", { symbol: symbol }
end
def stock_price(
symbol : String, # MSFT
exchange : String, # SMART
currency : String, # USD
data_type : MarketDataType = :realtime
) : SnapshotPrice
SnapshotPrice.from_json http_get "/api/v1/stock_price",
{ symbol: symbol, exchange: exchange, currency: currency, data_type: data_type.to_s.underscore }
end
record SnapshotPrice,
last_price : Float?,
close_price : Float?,
ask_price : Float?,
bid_price : Float?,
approximate_price : Float,
data_type : MarketDataType,
do
include JSON::Serializable
end
def stock_option_chains(
symbol : String, # MSFT
exchange : String, # SMART
currency : String, # USD
) : OptionChains
OptionChains.from_json http_get "/api/v1/stock_option_chains",
{ symbol: symbol, exchange: exchange, currency: currency }
end
record OptionChain,
option_exchange : String,
expirations_asc : Array(String), # Sorted
strikes_asc : Array(Float), # Sorted
multiplier : Int, # Multiplier 100 or 1000
do
include JSON::Serializable
end
record OptionChains,
largest_desc : Array(OptionChain),
# Different exchanges could have different stock option chains, with different amount of contracts,
# sorting desc by contract amount.
all : Array(OptionChain),
do
include JSON::Serializable
end
def stock_option_chain(
symbol : String, # MSFT
exchange : String, # SMART
option_exchange : String, # AMEX, differnt from the stock exchange
currency : String, # USD
) : OptionChain
chains = stock_option_chains(symbol, exchange, currency)
ochain = chains.largest_desc.find { |chain| chain.option_exchange == option_exchange }
raise "chain for exhange #{option_exchange} not found" if ochain.nil?
ochain
end
def stock_option_chain_contracts(
symbol : String, # MSFT
option_exchange : String, # AMEX, differnt from the stock exchange
currency : String, # USD
): OptionContracts
OptionContracts.from_json http_get "/api/v1/stock_option_chain_contracts",
{ symbol: symbol, option_exchange: option_exchange, currency: currency }
end
record OptionContract,
right : Right,
expiration : String, # 2020-08-21
strike : Float, # 120
do
include JSON::Serializable
end
record OptionContractWithId,
id : Int,
expiration : String, # 2020-08-21
strike : Float, # 120
right : Right,
do
include JSON::Serializable
end
record OptionContracts,
multiplier : Int, # 100 or 1000
contracts_asc_by_right_expiration_strike : Array(OptionContractWithId), # Sorted
do
include JSON::Serializable
end
def stock_option_chain_contracts_by_expirations(
symbol : String, # MSFT
option_exchange : String, # AMEX, differnt from the stock exchange
currency : String, # USD
expirations : Array(String) # 2020-01-01
): Array(OptionContractWithId)
requests = expirations.map { |expiration| {
path: "/api/v1/stock_option_chain_contracts_by_expiration",
body: { symbol: symbol, option_exchange: option_exchange, currency: currency, expiration: expiration }
}}
http_post_batch("/api/v1/call", requests, Array(OptionContractWithId)).map do |r|
raise r if r.is_a? Exception
r
end
.flatten
end
def stock_options_prices(
contracts : Array(StockOptionParams),
): Array(Exception | SnapshotPrice)
requests = contracts.map { |params| { path: "/api/v1/stock_option_price", body: params } }
http_post_batch("/api/v1/call", requests, SnapshotPrice)
end
record StockOptionParams,
symbol : String, # MSFT
right : Right, # "put" or "call"'
expiration : String, # 2020-08-21
strike : Float, # 120
option_exchange : String, # AMEX, option exchange, different from the stock exchange
currency : String, # USD
data_type : MarketDataType,
do
include JSON::Serializable
end
def portfolio : Array(Portfolio)
Array(Portfolio).from_json http_get "/api/v1/portfolio"
end
record PortfolioStockContract,
symbol : String,
exchange : String?, # IB dosn't always provide it
currency : String,
id : Int, # IB id for contract
do
include JSON::Serializable
end
record PortfolioOptionContract,
symbol : String,
right : Right, # "put" or "call"'
expiration : String, # 2020-08-21
strike : Float, # 120
exchange : String?, # IB dosn't always provide it
currency : String,
id : Int, # IB id for contract
multiplier : Int, # Usually 100
do
include JSON::Serializable
end
record StockPortfolioPosition,
position : Int,
average_cost : Float,
contract : PortfolioStockContract,
do
include JSON::Serializable
end
record OptionPortfolioPosition,
position : Int,
average_cost : Float,
contract : PortfolioOptionContract,
do
include JSON::Serializable
end
record Portfolio,
account_id : String,
stocks : Array(StockPortfolioPosition),
stock_options : Array(OptionPortfolioPosition),
cash_in_usd : Float,
do
include JSON::Serializable
end
# Helpers ----------------------------------------------------------------------------------------
protected def http_get(path : String, query = NamedTuple.new())
resp = HTTP::Client.get @base_url + path + '?' + URI::Params.encode(query)
unless resp.success?
raise "can't get #{path} #{query}, #{resp.headers["error"]? || resp.body? || "unknown error"}"
end
resp.body
end
protected def http_post_batch(
path : String,
requests : Array(NamedTuple(path: String, body: Object)),
klass : T.class
) : Array(T | Exception) forall T
resp = HTTP::Client.post @base_url + path, body: requests.to_json
unless resp.success?
raise "can't post batch #{path}, #{resp.headers["message"]? || resp.body? || "unknown error"}"
end
parts = JSON.parse(resp.body); i = 0; results = [] of T | Exception;
while i < parts.size
part = parts[i]; i += 1
is_errorneous_wrapper = begin
part["is_error"] # it throws an error if it's not object or doesn't have `is_error`
true
rescue
false
end
if is_errorneous_wrapper
if part["is_error"].as_bool
results << Exception.new(part["error"].as_s)
else
results << klass.from_json(part["value"].to_json)
end
else
results << klass.from_json(part.to_json)
end
end
results
end
end