forked from robcarver17/pysystemtrade
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathib_contracts_client.py
603 lines (498 loc) · 21.6 KB
/
ib_contracts_client.py
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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
from copy import copy
from ib_insync import Contract
from syscore.constants import missing_contract
from syscore.cache import Cache
from syscore.exceptions import missingData, missingContract
from sysbrokers.IB.client.ib_client import ibClient
from sysbrokers.IB.ib_instruments import (
ib_futures_instrument_just_symbol,
futuresInstrumentWithIBConfigData,
ib_futures_instrument,
)
from sysbrokers.IB.ib_trading_hours import (
get_trading_hours_from_contract_details,
get_saved_trading_hours,
)
from sysbrokers.IB.ib_contracts import (
ibcontractWithLegs,
get_ib_contract_with_specific_expiry,
resolve_unique_contract_from_ibcontract_list,
_add_legs_to_ib_contract,
)
from syslogdiag.pst_logger import pst_logger
from sysobjects.contracts import futuresContract, contractDate
from sysobjects.production.trading_hours.intersection_of_weekly_and_specific_trading_hours import (
intersection_of_any_weekly_and_list_of_normal_trading_hours,
)
from sysobjects.production.trading_hours.dict_of_weekly_trading_hours_any_day import (
dictOfDictOfWeekdayTradingHours,
)
from sysobjects.production.trading_hours.weekly_trading_hours_any_day import (
weekdayDictOfListOfTradingHoursAnyDay,
)
from sysobjects.production.trading_hours.trading_hours import listOfTradingHours
from sysexecution.trade_qty import tradeQuantity
class ibContractsClient(ibClient):
def broker_get_futures_contract_list(
self,
futures_instrument_with_ib_data: futuresInstrumentWithIBConfigData,
allow_expired: bool = False,
) -> list:
## Returns list of contract date strings YYYYMMDD
specific_log = self.log.setup(
instrument_code=futures_instrument_with_ib_data.instrument_code
)
ibcontract_pattern = ib_futures_instrument(futures_instrument_with_ib_data)
contract_list = self.ib_get_contract_chain(
ibcontract_pattern, allow_expired=allow_expired
)
# if no contracts found will be empty
# Extract expiry date strings from these
contract_dates = [
ibcontract.lastTradeDateOrContractMonth for ibcontract in contract_list
]
return contract_dates
def broker_get_single_contract_expiry_date(
self,
futures_contract_with_ib_data: futuresContract,
allow_expired: bool = False,
) -> str:
"""
Return the exact expiry date for a given contract
:param futures_contract_with_ib_data: contract where instrument has ib metadata
:return: YYYYMMDD str
"""
specific_log = futures_contract_with_ib_data.specific_log(self.log)
if futures_contract_with_ib_data.is_spread_contract():
specific_log.warn("Can only find expiry for single leg contract!")
raise missingContract
try:
ibcontract = self.ib_futures_contract(
futures_contract_with_ib_data,
allow_expired=allow_expired,
always_return_single_leg=True,
)
except missingContract:
specific_log.warn("Contract is missing can't get expiry")
raise missingContract
expiry_date = ibcontract.lastTradeDateOrContractMonth
return expiry_date
def ib_get_trading_hours(
self, contract_object_with_ib_data: futuresContract
) -> listOfTradingHours:
## Expensive calculations so we cache
return self.cache.get(
self._ib_get_uncached_trading_hours, contract_object_with_ib_data
)
def _ib_get_uncached_trading_hours(
self, contract_object_with_ib_data: futuresContract
) -> listOfTradingHours:
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
trading_hours_from_ib = self.ib_get_trading_hours_from_IB(
contract_object_with_ib_data
)
except Exception as e:
specific_log.warn(
"%s when getting trading hours from %s!"
% (str(e), str(contract_object_with_ib_data))
)
raise missingData
try:
saved_weekly_trading_hours = (
self.ib_get_saved_weekly_trading_hours_for_contract(
contract_object_with_ib_data
)
)
except:
## no saved hours, use IB
return trading_hours_from_ib
## OK use the intersection
trading_hours = intersection_of_any_weekly_and_list_of_normal_trading_hours(
trading_hours_from_ib, saved_weekly_trading_hours
)
return trading_hours
def ib_get_trading_hours_from_IB(
self, contract_object_with_ib_data: futuresContract
) -> listOfTradingHours:
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
ib_contract_details = self.ib_get_contract_details(
contract_object_with_ib_data
)
trading_hours_from_ib = get_trading_hours_from_contract_details(
ib_contract_details
)
except Exception as e:
specific_log.warn(
"%s when getting trading hours from %s!"
% (str(e), str(contract_object_with_ib_data))
)
raise missingData
return trading_hours_from_ib
def ib_get_saved_weekly_trading_hours_for_contract(
self, contract_object_with_ib_data: futuresContract
) -> weekdayDictOfListOfTradingHoursAnyDay:
try:
weekly_hours_for_timezone = (
self.ib_get_saved_weekly_trading_hours_for_timezone_of_contract(
contract_object_with_ib_data
)
)
except missingData:
weekly_hours_for_timezone = None
try:
specific_weekly_hours_for_contract = (
self.ib_get_saved_weekly_trading_hours_custom_for_contract(
contract_object_with_ib_data
)
)
except missingData:
specific_weekly_hours_for_contract = None
if (
specific_weekly_hours_for_contract is None
and weekly_hours_for_timezone is None
):
raise missingData
if specific_weekly_hours_for_contract is None:
return weekly_hours_for_timezone
if weekly_hours_for_timezone is None:
return specific_weekly_hours_for_contract
intersected_trading_hours = weekly_hours_for_timezone.intersect(
specific_weekly_hours_for_contract
)
return intersected_trading_hours
def ib_get_saved_weekly_trading_hours_custom_for_contract(
self, contract_object_with_ib_data: futuresContract
) -> weekdayDictOfListOfTradingHoursAnyDay:
instrument_code = contract_object_with_ib_data.instrument_code
all_saved_trading_hours = self.get_all_saved_weekly_trading_hours()
specific_weekly_hours_for_contract = all_saved_trading_hours.get(
instrument_code, None
)
if specific_weekly_hours_for_contract is None:
# no warning necessary this is normal
empty_hours = weekdayDictOfListOfTradingHoursAnyDay.create_empty()
raise missingData
return specific_weekly_hours_for_contract
def ib_get_saved_weekly_trading_hours_for_timezone_of_contract(
self, contract_object_with_ib_data: futuresContract
) -> weekdayDictOfListOfTradingHoursAnyDay:
specific_log = contract_object_with_ib_data.log(self.log)
try:
time_zone_id = self.ib_get_timezoneid(contract_object_with_ib_data)
except missingData:
# problem getting timezoneid
specific_log.warn(
"No time zone ID, can't get trading hours for timezone for %s"
% str(contract_object_with_ib_data)
)
raise missingData
all_saved_trading_hours = self.get_all_saved_weekly_trading_hours()
weekly_hours_for_timezone = all_saved_trading_hours.get(time_zone_id, None)
if weekly_hours_for_timezone is None:
# this means IB have changed something critical or missing file so we bork and alert
error_msg = (
"Check ib_config_trading_hours in sysbrokers/IB or private directory, hours for timezone %s not found!"
% time_zone_id
)
specific_log.log.critical(error_msg)
raise missingData
return weekly_hours_for_timezone
def ib_get_timezoneid(self, contract_object_with_ib_data: futuresContract) -> str:
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
ib_contract_details = self.ib_get_contract_details(
contract_object_with_ib_data
)
time_zone_id = ib_contract_details.timeZoneId
except Exception as e:
specific_log.warn(
"%s when getting time zone from %s!"
% (str(e), str(contract_object_with_ib_data))
)
raise missingData
return time_zone_id
def get_all_saved_weekly_trading_hours(self) -> dictOfDictOfWeekdayTradingHours:
return self.cache.get(self._get_all_saved_weekly_trading_hours_from_file)
def _get_all_saved_weekly_trading_hours_from_file(self):
try:
saved_hours = get_saved_trading_hours()
except:
self.log.critical(
"Saved trading hours file missing - will use only IB hours"
)
return dictOfDictOfWeekdayTradingHours({})
return saved_hours
def ib_get_min_tick_size(
self, contract_object_with_ib_data: futuresContract
) -> float:
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
ib_contract = self.ib_futures_contract(
contract_object_with_ib_data, always_return_single_leg=True
)
except missingContract:
specific_log.warn("Can't get tick size as contract missing")
raise
ib_contract_details = self.ib.reqContractDetails(ib_contract)[0]
try:
min_tick = ib_contract_details.minTick
except Exception as e:
specific_log.warn(
"%s when getting min tick size from %s!"
% (str(e), str(ib_contract_details))
)
raise missingContract
return min_tick
def ib_get_price_magnifier(
self, contract_object_with_ib_data: futuresContract
) -> float:
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
ib_contract = self.ib_futures_contract(
contract_object_with_ib_data, always_return_single_leg=True
)
except missingContract:
specific_log.warn("Can't get price magnifier as contract missing")
raise
ib_contract_details = self.ib.reqContractDetails(ib_contract)[0]
try:
price_magnifier = ib_contract_details.priceMagnifier
except Exception as e:
specific_log.warn(
"%s when getting price magnifier from %s!"
% (str(e), str(ib_contract_details))
)
raise missingContract
return price_magnifier
def ib_get_contract_details(self, contract_object_with_ib_data: futuresContract):
specific_log = contract_object_with_ib_data.specific_log(self.log)
try:
ib_contract = self.ib_futures_contract(
contract_object_with_ib_data, always_return_single_leg=True
)
except missingContract:
specific_log.warn("Can't get trading hours as contract is missing")
raise
# returns a list but should only have one element
ib_contract_details_list = self.ib.reqContractDetails(ib_contract)
ib_contract_details = ib_contract_details_list[0]
return ib_contract_details
def ib_futures_contract(
self,
futures_contract_with_ib_data: futuresContract,
allow_expired=False,
always_return_single_leg=False,
trade_list_for_multiple_legs: tradeQuantity = None,
) -> Contract:
ibcontract_with_legs = self.ib_futures_contract_with_legs(
futures_contract_with_ib_data=futures_contract_with_ib_data,
allow_expired=allow_expired,
always_return_single_leg=always_return_single_leg,
trade_list_for_multiple_legs=trade_list_for_multiple_legs,
)
return ibcontract_with_legs.ibcontract
def ib_futures_contract_with_legs(
self,
futures_contract_with_ib_data: futuresContract,
allow_expired: bool = False,
always_return_single_leg: bool = False,
trade_list_for_multiple_legs: tradeQuantity = None,
) -> ibcontractWithLegs:
"""
Return a complete and unique IB contract that matches contract_object_with_ib_data
Doesn't actually get the data from IB, tries to get from cache
:param futures_contract_with_ib_data: contract, containing instrument metadata suitable for IB
:return: a single ib contract object
"""
contract_object_to_use = copy(futures_contract_with_ib_data)
if always_return_single_leg and contract_object_to_use.is_spread_contract():
contract_object_to_use = (
contract_object_to_use.new_contract_with_first_contract_date()
)
ibcontract_with_legs = self._get_stored_or_live_contract(
contract_object_to_use=contract_object_to_use,
trade_list_for_multiple_legs=trade_list_for_multiple_legs,
allow_expired=allow_expired,
)
return ibcontract_with_legs
def _get_stored_or_live_contract(
self,
contract_object_to_use: futuresContract,
trade_list_for_multiple_legs: tradeQuantity = None,
allow_expired: bool = False,
):
ibcontract_with_legs = self.cache.get(
self._get_ib_futures_contract_from_broker,
contract_object_to_use,
trade_list_for_multiple_legs=trade_list_for_multiple_legs,
allow_expired=allow_expired,
)
return ibcontract_with_legs
## FIXME USE GENERIC CACHING CODE
@property
def cache(self) -> Cache:
## dynamically create because don't have access to __init__ method
cache = getattr(self, "_cache", None)
if cache is None:
cache = self._cache = Cache(self)
return cache
def _get_ib_futures_contract_from_broker(
self,
contract_object_with_ib_data: futuresContract,
trade_list_for_multiple_legs: tradeQuantity = None,
allow_expired: bool = False,
) -> ibcontractWithLegs:
"""
Return a complete and unique IB contract that matches futures_contract_object
This is expensive so not called directly, only by ib_futures_contract which does caching
:param contract_object_with_ib_data: contract, containing instrument metadata suitable for IB
:return: a single ib contract object
"""
# Convert to IB world
futures_instrument_with_ib_data = contract_object_with_ib_data.instrument
contract_date = contract_object_with_ib_data.contract_date
if contract_object_with_ib_data.is_spread_contract():
ibcontract_with_legs = self._get_spread_ib_futures_contract(
futures_instrument_with_ib_data,
contract_date,
allow_expired=allow_expired,
trade_list_for_multiple_legs=trade_list_for_multiple_legs,
)
else:
ibcontract_with_legs = self._get_vanilla_ib_futures_contract_with_legs(
futures_instrument_with_ib_data=futures_instrument_with_ib_data,
allow_expired=allow_expired,
contract_date=contract_date,
)
return ibcontract_with_legs
def _get_vanilla_ib_futures_contract_with_legs(
self,
futures_instrument_with_ib_data: futuresInstrumentWithIBConfigData,
contract_date: contractDate,
allow_expired: bool = False,
) -> ibcontractWithLegs:
ibcontract = self._get_vanilla_ib_futures_contract(
futures_instrument_with_ib_data, contract_date, allow_expired=allow_expired
)
legs = []
return ibcontractWithLegs(ibcontract, legs)
def _get_spread_ib_futures_contract(
self,
futures_instrument_with_ib_data: futuresInstrumentWithIBConfigData,
contract_date: contractDate,
trade_list_for_multiple_legs: tradeQuantity = None,
allow_expired: bool = False,
) -> ibcontractWithLegs:
"""
Return a complete and unique IB contract that matches contract_object_with_ib_data
This is expensive so not called directly, only by ib_futures_contract which does caching
:param contract_object_with_ib_data: contract, containing instrument metadata suitable for IB
:return: a single ib contract object
"""
if trade_list_for_multiple_legs is None:
raise Exception("Multiple leg order must have trade list")
# Convert to IB world
ibcontract = ib_futures_instrument(futures_instrument_with_ib_data)
ibcontract.secType = "BAG"
list_of_contract_dates = contract_date.list_of_single_contract_dates
resolved_legs = [
self._get_vanilla_ib_futures_contract(
futures_instrument_with_ib_data,
contract_date,
allow_expired=allow_expired,
)
for contract_date in list_of_contract_dates
]
ibcontract_with_legs = _add_legs_to_ib_contract(
ibcontract=ibcontract,
resolved_legs=resolved_legs,
trade_list_for_multiple_legs=trade_list_for_multiple_legs,
)
return ibcontract_with_legs
def _get_vanilla_ib_futures_contract(
self,
futures_instrument_with_ib_data: futuresInstrumentWithIBConfigData,
contract_date: contractDate,
allow_expired: bool = False,
) -> Contract:
"""
Return a complete and unique IB contract that matches contract_object_with_ib_data
This is expensive so not called directly, only by ib_futures_contract which does caching
:param contract_object_with_ib_data: contract, containing instrument metadata suitable for IB
:return: a single ib contract object
"""
ibcontract = get_ib_contract_with_specific_expiry(
contract_date=contract_date,
futures_instrument_with_ib_data=futures_instrument_with_ib_data,
)
# We could get multiple contracts here in case we have 'yyyymm' and not
# specified expiry date for VIX
ibcontract_list = self.ib_get_contract_chain(
ibcontract, allow_expired=allow_expired
)
try:
resolved_contract = resolve_unique_contract_from_ibcontract_list(
ibcontract_list=ibcontract_list,
futures_instrument_with_ib_data=futures_instrument_with_ib_data,
)
except Exception as exception:
self.log.warn(
"%s could not resolve contracts: %s"
% (str(futures_instrument_with_ib_data), exception.args[0])
)
raise missingContract
return resolved_contract
def ib_resolve_unique_contract(self, ibcontract_pattern, log: pst_logger = None):
"""
Returns the 'resolved' IB contract based on a pattern. We expect a unique contract.
This is used for FX only, since for futures things are potentially funkier
:param ibcontract_pattern: ibContract
:param log: log object
:return: ibContract or missing_contract
"""
if log is None:
log = self.log
contract_chain = self.ib_get_contract_chain(ibcontract_pattern)
if len(contract_chain) > 1:
log.warn(
"Got multiple contracts for %s when only expected a single contract: Check contract date"
% str(ibcontract_pattern)
)
raise missingContract
if len(contract_chain) == 0:
log.warn("Failed to resolve contract %s" % str(ibcontract_pattern))
raise missingContract
resolved_contract = contract_chain[0]
return resolved_contract
def ib_get_contract_with_conId(self, symbol: str, conId) -> Contract:
contract_chain = self._get_contract_chain_for_symbol(symbol)
conId_list = [contract.conId for contract in contract_chain]
try:
contract_idx = conId_list.index(conId)
except ValueError:
raise missingContract
required_contract = contract_chain[contract_idx]
return required_contract
def _get_contract_chain_for_symbol(self, symbol: str) -> list:
ibcontract_pattern = ib_futures_instrument_just_symbol(symbol)
contract_chain = self.ib_get_contract_chain(ibcontract_pattern)
return contract_chain
def ib_get_contract_chain(
self, ibcontract_pattern: Contract, allow_expired: bool = False
) -> list:
"""
Get all the IB contracts matching a pattern.
:param ibcontract_pattern: ibContract which may not fully specify the contract
:return: list of ibContracts
"""
new_contract_details_list = self.get_contract_details(
ibcontract_pattern,
allow_expired=allow_expired,
allow_multiple_contracts=True,
)
ibcontract_list = [
contract_details.contract for contract_details in new_contract_details_list
]
return ibcontract_list