32
32
from nautilus_trader .model .identifiers import InstrumentId
33
33
from nautilus_trader .model .identifiers import Symbol
34
34
from nautilus_trader .model .identifiers import Venue
35
+ from nautilus_trader .model .instruments import Cfd
36
+ from nautilus_trader .model .instruments import Commodity
35
37
from nautilus_trader .model .instruments import CryptoPerpetual
36
38
from nautilus_trader .model .instruments import CurrencyPair
37
39
from nautilus_trader .model .instruments import Equity
74
76
"NYBOT" , # US
75
77
"SNFE" , # AU
76
78
]
79
+ VENUES_CFD = [
80
+ "IBCFD" , # self named, in fact mapping to "SMART" when parsing
81
+ ]
82
+ VENUES_CMDTY = ["IBCMDTY" ] # self named, in fact mapping to "SMART" when parsing
77
83
78
84
RE_CASH = re .compile (r"^(?P<symbol>[A-Z]{3})\/(?P<currency>[A-Z]{3})$" )
85
+ RE_CFD_CASH = re .compile (r"^(?P<symbol>[A-Z]{3})\.(?P<currency>[A-Z]{3})$" )
79
86
RE_OPT = re .compile (
80
87
r"^(?P<symbol>^[A-Z]{1,6})(?P<expiry>\d{6})(?P<right>[CP])(?P<strike>\d{5})(?P<decimal>\d{3})$" ,
81
88
)
@@ -116,6 +123,7 @@ def sec_type_to_asset_class(sec_type: str) -> AssetClass:
116
123
"IND" : "INDEX" ,
117
124
"CASH" : "FX" ,
118
125
"BOND" : "DEBT" ,
126
+ "CMDTY" : "COMMODITY" ,
119
127
}
120
128
return asset_class_from_str (mapping .get (sec_type , sec_type ))
121
129
@@ -145,6 +153,10 @@ def parse_instrument(
145
153
return parse_forex_contract (details = contract_details , instrument_id = instrument_id )
146
154
elif security_type == "CRYPTO" :
147
155
return parse_crypto_contract (details = contract_details , instrument_id = instrument_id )
156
+ elif security_type == "CFD" :
157
+ return parse_cfd_contract (details = contract_details , instrument_id = instrument_id )
158
+ elif security_type == "CMDTY" :
159
+ return parse_commodity_contract (details = contract_details , instrument_id = instrument_id )
148
160
else :
149
161
raise ValueError (f"Unknown { security_type = } " )
150
162
@@ -319,6 +331,99 @@ def parse_crypto_contract(
319
331
)
320
332
321
333
334
+ def parse_cfd_contract (
335
+ details : IBContractDetails ,
336
+ instrument_id : InstrumentId ,
337
+ ) -> Cfd :
338
+ price_precision : int = _tick_size_to_precision (details .minTick )
339
+ size_precision : int = _tick_size_to_precision (details .minSize )
340
+ timestamp = time .time_ns ()
341
+ if RE_CFD_CASH .match (details .contract .localSymbol ):
342
+ return Cfd (
343
+ instrument_id = instrument_id ,
344
+ raw_symbol = Symbol (details .contract .localSymbol ),
345
+ asset_class = sec_type_to_asset_class (details .underSecType ),
346
+ base_currency = Currency .from_str (details .contract .symbol ),
347
+ quote_currency = Currency .from_str (details .contract .currency ),
348
+ price_precision = price_precision ,
349
+ size_precision = size_precision ,
350
+ price_increment = Price (details .minTick , price_precision ),
351
+ size_increment = Quantity (details .sizeIncrement , size_precision ),
352
+ lot_size = None ,
353
+ max_quantity = None ,
354
+ min_quantity = None ,
355
+ max_notional = None ,
356
+ min_notional = None ,
357
+ max_price = None ,
358
+ min_price = None ,
359
+ margin_init = Decimal (0 ),
360
+ margin_maint = Decimal (0 ),
361
+ maker_fee = Decimal (0 ),
362
+ taker_fee = Decimal (0 ),
363
+ ts_event = timestamp ,
364
+ ts_init = timestamp ,
365
+ info = contract_details_to_dict (details ),
366
+ )
367
+ else :
368
+ return Cfd (
369
+ instrument_id = instrument_id ,
370
+ raw_symbol = Symbol (details .contract .localSymbol ),
371
+ asset_class = sec_type_to_asset_class (details .underSecType ),
372
+ quote_currency = Currency .from_str (details .contract .currency ),
373
+ price_precision = price_precision ,
374
+ size_precision = size_precision ,
375
+ price_increment = Price (details .minTick , price_precision ),
376
+ size_increment = Quantity (details .sizeIncrement , size_precision ),
377
+ lot_size = None ,
378
+ max_quantity = None ,
379
+ min_quantity = None ,
380
+ max_notional = None ,
381
+ min_notional = None ,
382
+ max_price = None ,
383
+ min_price = None ,
384
+ margin_init = Decimal (0 ),
385
+ margin_maint = Decimal (0 ),
386
+ maker_fee = Decimal (0 ),
387
+ taker_fee = Decimal (0 ),
388
+ ts_event = timestamp ,
389
+ ts_init = timestamp ,
390
+ info = contract_details_to_dict (details ),
391
+ )
392
+
393
+
394
+ def parse_commodity_contract (
395
+ details : IBContractDetails ,
396
+ instrument_id : InstrumentId ,
397
+ ) -> Commodity :
398
+ price_precision : int = _tick_size_to_precision (details .minTick )
399
+ size_precision : int = _tick_size_to_precision (details .minSize )
400
+ timestamp = time .time_ns ()
401
+ return Commodity (
402
+ instrument_id = instrument_id ,
403
+ raw_symbol = Symbol (details .contract .localSymbol ),
404
+ asset_class = AssetClass .COMMODITY ,
405
+ quote_currency = Currency .from_str (details .contract .currency ),
406
+ price_precision = price_precision ,
407
+ size_precision = size_precision ,
408
+ price_increment = Price (details .minTick , price_precision ),
409
+ size_increment = Quantity (details .sizeIncrement , size_precision ),
410
+ lot_size = None ,
411
+ max_quantity = None ,
412
+ min_quantity = None ,
413
+ max_notional = None ,
414
+ min_notional = None ,
415
+ max_price = None ,
416
+ min_price = None ,
417
+ margin_init = Decimal (0 ),
418
+ margin_maint = Decimal (0 ),
419
+ maker_fee = Decimal (0 ),
420
+ taker_fee = Decimal (0 ),
421
+ ts_event = timestamp ,
422
+ ts_init = timestamp ,
423
+ info = contract_details_to_dict (details ),
424
+ )
425
+
426
+
322
427
def decade_digit (last_digit : str , contract : IBContract ) -> int :
323
428
if year := contract .lastTradeDateOrContractMonth [:4 ]:
324
429
return int (year [2 :3 ])
@@ -341,12 +446,21 @@ def ib_contract_to_instrument_id(
341
446
342
447
343
448
def ib_contract_to_instrument_id_strict_symbology (contract : IBContract ) -> InstrumentId :
344
- symbol = f"{ contract .localSymbol } ={ contract .secType } "
345
- venue = (contract .primaryExchange or contract .exchange ).replace ("." , "/" )
449
+ if contract .secType == "CFD" :
450
+ symbol = f"{ contract .localSymbol } ={ contract .secType } "
451
+ venue = "IBCFD"
452
+ elif contract .secType == "CMDTY" :
453
+ symbol = f"{ contract .localSymbol } ={ contract .secType } "
454
+ venue = "IBCMDTY"
455
+ else :
456
+ symbol = f"{ contract .localSymbol } ={ contract .secType } "
457
+ venue = (contract .primaryExchange or contract .exchange ).replace ("." , "/" )
346
458
return InstrumentId .from_str (f"{ symbol } .{ venue } " )
347
459
348
460
349
- def ib_contract_to_instrument_id_simplified_symbology (contract : IBContract ) -> InstrumentId :
461
+ def ib_contract_to_instrument_id_simplified_symbology ( # noqa: C901 (too complex)
462
+ contract : IBContract ,
463
+ ) -> InstrumentId :
350
464
security_type = contract .secType
351
465
if security_type == "STK" :
352
466
symbol = (contract .localSymbol or contract .symbol ).replace (" " , "-" )
@@ -372,6 +486,19 @@ def ib_contract_to_instrument_id_simplified_symbology(contract: IBContract) -> I
372
486
f"{ contract .localSymbol } " .replace ("." , "/" ) or f"{ contract .symbol } /{ contract .currency } "
373
487
)
374
488
venue = contract .exchange
489
+ elif security_type == "CFD" :
490
+ if m := RE_CFD_CASH .match (contract .localSymbol ):
491
+ symbol = (
492
+ f"{ contract .localSymbol } " .replace ("." , "/" )
493
+ or f"{ contract .symbol } /{ contract .currency } "
494
+ )
495
+ venue = "IBCFD"
496
+ else :
497
+ symbol = (contract .symbol ).replace (" " , "-" )
498
+ venue = "IBCFD"
499
+ elif security_type == "CMDTY" :
500
+ symbol = (contract .symbol ).replace (" " , "-" )
501
+ venue = "IBCMDTY"
375
502
else :
376
503
symbol = None
377
504
venue = None
@@ -402,6 +529,18 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -
402
529
primaryExchange = exchange ,
403
530
localSymbol = local_symbol ,
404
531
)
532
+ elif security_type == "CFD" :
533
+ return IBContract (
534
+ secType = security_type ,
535
+ exchange = "SMART" ,
536
+ localSymbol = local_symbol , # by IB is a cfd's local symbol of STK with a "n" as tail, e.g. "NVDAn". "
537
+ )
538
+ elif security_type == "CMDTY" :
539
+ return IBContract (
540
+ secType = security_type ,
541
+ exchange = "SMART" ,
542
+ localSymbol = local_symbol ,
543
+ )
405
544
else :
406
545
return IBContract (
407
546
secType = security_type ,
@@ -410,7 +549,9 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -
410
549
)
411
550
412
551
413
- def instrument_id_to_ib_contract_simplified_symbology (instrument_id : InstrumentId ) -> IBContract :
552
+ def instrument_id_to_ib_contract_simplified_symbology ( # noqa: C901 (too complex)
553
+ instrument_id : InstrumentId ,
554
+ ) -> IBContract :
414
555
if instrument_id .venue .value in VENUES_CASH and (
415
556
m := RE_CASH .match (instrument_id .symbol .value )
416
557
):
@@ -464,6 +605,26 @@ def instrument_id_to_ib_contract_simplified_symbology(instrument_id: InstrumentI
464
605
)
465
606
else :
466
607
raise ValueError (f"Cannot parse { instrument_id } , use 2-digit year for FUT and FOP" )
608
+ elif instrument_id .venue .value in VENUES_CFD :
609
+ if m := RE_CASH .match (instrument_id .symbol .value ):
610
+ return IBContract (
611
+ secType = "CFD" ,
612
+ exchange = "SMART" ,
613
+ symbol = m ["symbol" ],
614
+ localSymbol = f"{ m ['symbol' ]} .{ m ['currency' ]} " ,
615
+ )
616
+ else :
617
+ return IBContract (
618
+ secType = "CFD" ,
619
+ exchange = "SMART" ,
620
+ symbol = f"{ instrument_id .symbol .value } " .replace ("-" , " " ),
621
+ )
622
+ elif instrument_id .venue .value in VENUES_CMDTY :
623
+ return IBContract (
624
+ secType = "CMDTY" ,
625
+ exchange = "SMART" ,
626
+ symbol = f"{ instrument_id .symbol .value } " .replace ("-" , " " ),
627
+ )
467
628
elif instrument_id .venue .value == "InteractiveBrokers" : # keep until a better approach
468
629
# This will allow to make Instrument request using IBContract from within Strategy
469
630
# and depending on the Strategy requirement
0 commit comments