Skip to content

Commit b008fb3

Browse files
authored
Add CFD and Commodity support for Interactive Brokers (#1604)
1 parent 1e34216 commit b008fb3

File tree

8 files changed

+886
-5
lines changed

8 files changed

+886
-5
lines changed

nautilus_trader/adapters/interactive_brokers/common.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,18 @@ class IBContract(NautilusConfig, frozen=True, repr_omit_defaults=True):
101101
102102
"""
103103

104-
secType: Literal["CASH", "STK", "OPT", "FUT", "FOP", "CONTFUT", "CRYPTO", ""] = ""
104+
secType: Literal[
105+
"CASH",
106+
"STK",
107+
"OPT",
108+
"FUT",
109+
"FOP",
110+
"CONTFUT",
111+
"CRYPTO",
112+
"CFD",
113+
"CMDTY",
114+
"",
115+
] = ""
105116
conId: int = 0
106117
exchange: str = ""
107118
primaryExchange: str = ""

nautilus_trader/adapters/interactive_brokers/parsing/instruments.py

Lines changed: 165 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from nautilus_trader.model.identifiers import InstrumentId
3333
from nautilus_trader.model.identifiers import Symbol
3434
from nautilus_trader.model.identifiers import Venue
35+
from nautilus_trader.model.instruments import Cfd
36+
from nautilus_trader.model.instruments import Commodity
3537
from nautilus_trader.model.instruments import CryptoPerpetual
3638
from nautilus_trader.model.instruments import CurrencyPair
3739
from nautilus_trader.model.instruments import Equity
@@ -74,8 +76,13 @@
7476
"NYBOT", # US
7577
"SNFE", # AU
7678
]
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
7783

7884
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})$")
7986
RE_OPT = re.compile(
8087
r"^(?P<symbol>^[A-Z]{1,6})(?P<expiry>\d{6})(?P<right>[CP])(?P<strike>\d{5})(?P<decimal>\d{3})$",
8188
)
@@ -116,6 +123,7 @@ def sec_type_to_asset_class(sec_type: str) -> AssetClass:
116123
"IND": "INDEX",
117124
"CASH": "FX",
118125
"BOND": "DEBT",
126+
"CMDTY": "COMMODITY",
119127
}
120128
return asset_class_from_str(mapping.get(sec_type, sec_type))
121129

@@ -145,6 +153,10 @@ def parse_instrument(
145153
return parse_forex_contract(details=contract_details, instrument_id=instrument_id)
146154
elif security_type == "CRYPTO":
147155
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)
148160
else:
149161
raise ValueError(f"Unknown {security_type=}")
150162

@@ -319,6 +331,99 @@ def parse_crypto_contract(
319331
)
320332

321333

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+
322427
def decade_digit(last_digit: str, contract: IBContract) -> int:
323428
if year := contract.lastTradeDateOrContractMonth[:4]:
324429
return int(year[2:3])
@@ -341,12 +446,21 @@ def ib_contract_to_instrument_id(
341446

342447

343448
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(".", "/")
346458
return InstrumentId.from_str(f"{symbol}.{venue}")
347459

348460

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:
350464
security_type = contract.secType
351465
if security_type == "STK":
352466
symbol = (contract.localSymbol or contract.symbol).replace(" ", "-")
@@ -372,6 +486,19 @@ def ib_contract_to_instrument_id_simplified_symbology(contract: IBContract) -> I
372486
f"{contract.localSymbol}".replace(".", "/") or f"{contract.symbol}/{contract.currency}"
373487
)
374488
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"
375502
else:
376503
symbol = None
377504
venue = None
@@ -402,6 +529,18 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -
402529
primaryExchange=exchange,
403530
localSymbol=local_symbol,
404531
)
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+
)
405544
else:
406545
return IBContract(
407546
secType=security_type,
@@ -410,7 +549,9 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -
410549
)
411550

412551

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:
414555
if instrument_id.venue.value in VENUES_CASH and (
415556
m := RE_CASH.match(instrument_id.symbol.value)
416557
):
@@ -464,6 +605,26 @@ def instrument_id_to_ib_contract_simplified_symbology(instrument_id: InstrumentI
464605
)
465606
else:
466607
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+
)
467628
elif instrument_id.venue.value == "InteractiveBrokers": # keep until a better approach
468629
# This will allow to make Instrument request using IBContract from within Strategy
469630
# and depending on the Strategy requirement

nautilus_trader/model/instruments/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from nautilus_trader.model.instruments.base import Instrument
2121
from nautilus_trader.model.instruments.base import instruments_from_pyo3
2222
from nautilus_trader.model.instruments.betting import BettingInstrument
23+
from nautilus_trader.model.instruments.cfd import Cfd
24+
from nautilus_trader.model.instruments.commodity import Commodity
2325
from nautilus_trader.model.instruments.crypto_future import CryptoFuture
2426
from nautilus_trader.model.instruments.crypto_perpetual import CryptoPerpetual
2527
from nautilus_trader.model.instruments.currency_pair import CurrencyPair
@@ -42,6 +44,8 @@
4244
"FuturesSpread",
4345
"OptionsContract",
4446
"OptionsSpread",
47+
"Cfd",
48+
"Commodity",
4549
"SyntheticInstrument",
4650
"instruments_from_pyo3",
4751
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# -------------------------------------------------------------------------------------------------
2+
# Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
3+
# https://nautechsystems.io
4+
#
5+
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6+
# You may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# -------------------------------------------------------------------------------------------------
15+
16+
from nautilus_trader.model.instruments.base cimport Instrument
17+
18+
19+
cdef class Cfd(Instrument):
20+
cdef readonly str isin
21+
"""The instruments International Securities Identification Number (ISIN).\n\n:returns: `str` or ``None``"""
22+
23+
@staticmethod
24+
cdef Cfd from_dict_c(dict values)
25+
26+
@staticmethod
27+
cdef dict to_dict_c(Cfd obj)
28+
29+
@staticmethod
30+
cdef Cfd from_pyo3_c(pyo3_instrument)

0 commit comments

Comments
 (0)