Skip to content

Commit

Permalink
add CFD and Commodity support for Interactivebrokers; fix the strict_…
Browse files Browse the repository at this point in the history
…symbology for parsing Instrument/contract of Interactivebrokers

Signed-off-by: D <[email protected]>
  • Loading branch information
D authored and D committed Apr 23, 2024
1 parent ca66fdf commit 1dae661
Show file tree
Hide file tree
Showing 8 changed files with 874 additions and 3 deletions.
4 changes: 3 additions & 1 deletion nautilus_trader/adapters/interactive_brokers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ class IBContract(NautilusConfig, frozen=True, repr_omit_defaults=True):
"""

secType: Literal["CASH", "STK", "OPT", "FUT", "FOP", "CONTFUT", "CRYPTO", ""] = ""
secType: Literal[
"CASH", "STK", "OPT", "FUT", "FOP", "CONTFUT", "CRYPTO", "CFD", "CMDTY", ""
] = ""
conId: int = 0
exchange: str = ""
primaryExchange: str = ""
Expand Down
164 changes: 162 additions & 2 deletions nautilus_trader/adapters/interactive_brokers/parsing/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.identifiers import Symbol
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.instruments import Cfd
from nautilus_trader.model.instruments import Commodity
from nautilus_trader.model.instruments import CryptoPerpetual
from nautilus_trader.model.instruments import CurrencyPair
from nautilus_trader.model.instruments import Equity
Expand Down Expand Up @@ -74,8 +76,16 @@
"NYBOT", # US
"SNFE", # AU
]
VENUES_CFD = [
"IBCFD", # self named, in fact mapping to "SMART" when parsing
"IBUSCFD", # US
"IBCFD", # EU
"IBAPCFD", # Asia-Pacific
]
VENUES_CMDTY = ["IBCMDTY"] # self named, in fact mapping to "SMART" when parsing

RE_CASH = re.compile(r"^(?P<symbol>[A-Z]{3})\/(?P<currency>[A-Z]{3})$")
RE_CFD_CASH = re.compile(r"^(?P<symbol>[A-Z]{3})\.(?P<currency>[A-Z]{3})$")
RE_OPT = re.compile(
r"^(?P<symbol>^[A-Z]{1,6})(?P<expiry>\d{6})(?P<right>[CP])(?P<strike>\d{5})(?P<decimal>\d{3})$",
)
Expand Down Expand Up @@ -116,6 +126,7 @@ def sec_type_to_asset_class(sec_type: str) -> AssetClass:
"IND": "INDEX",
"CASH": "FX",
"BOND": "DEBT",
"CMDTY": "COMMODITY",
}
return asset_class_from_str(mapping.get(sec_type, sec_type))

Expand Down Expand Up @@ -145,6 +156,10 @@ def parse_instrument(
return parse_forex_contract(details=contract_details, instrument_id=instrument_id)
elif security_type == "CRYPTO":
return parse_crypto_contract(details=contract_details, instrument_id=instrument_id)
elif security_type == "CFD":
return parse_cfd_contract(details=contract_details, instrument_id=instrument_id)
elif security_type == "CMDTY":
return parse_commodity_contract(details=contract_details, instrument_id=instrument_id)
else:
raise ValueError(f"Unknown {security_type=}")

Expand Down Expand Up @@ -319,6 +334,99 @@ def parse_crypto_contract(
)


def parse_cfd_contract(
details: IBContractDetails,
instrument_id: InstrumentId,
) -> Cfd:
price_precision: int = _tick_size_to_precision(details.minTick)
size_precision: int = _tick_size_to_precision(details.minSize)
timestamp = time.time_ns()
if RE_CFD_CASH.match(details.contract.localSymbol):
return Cfd(
instrument_id=instrument_id,
raw_symbol=Symbol(details.contract.localSymbol),
asset_class=sec_type_to_asset_class(details.underSecType),
base_currency=Currency.from_str(details.contract.symbol),
quote_currency=Currency.from_str(details.contract.currency),
price_precision=price_precision,
size_precision=size_precision,
price_increment=Price(details.minTick, price_precision),
size_increment=Quantity(details.sizeIncrement, size_precision),
lot_size=None,
max_quantity=None,
min_quantity=None,
max_notional=None,
min_notional=None,
max_price=None,
min_price=None,
margin_init=Decimal(0),
margin_maint=Decimal(0),
maker_fee=Decimal(0),
taker_fee=Decimal(0),
ts_event=timestamp,
ts_init=timestamp,
info=contract_details_to_dict(details),
)
else:
return Cfd(
instrument_id=instrument_id,
raw_symbol=Symbol(details.contract.localSymbol),
asset_class=sec_type_to_asset_class(details.underSecType),
quote_currency=Currency.from_str(details.contract.currency),
price_precision=price_precision,
size_precision=size_precision,
price_increment=Price(details.minTick, price_precision),
size_increment=Quantity(details.sizeIncrement, size_precision),
lot_size=None,
max_quantity=None,
min_quantity=None,
max_notional=None,
min_notional=None,
max_price=None,
min_price=None,
margin_init=Decimal(0),
margin_maint=Decimal(0),
maker_fee=Decimal(0),
taker_fee=Decimal(0),
ts_event=timestamp,
ts_init=timestamp,
info=contract_details_to_dict(details),
)


def parse_commodity_contract(
details: IBContractDetails,
instrument_id: InstrumentId,
) -> Commodity:
price_precision: int = _tick_size_to_precision(details.minTick)
size_precision: int = _tick_size_to_precision(details.minSize)
timestamp = time.time_ns()
return Commodity(
instrument_id=instrument_id,
raw_symbol=Symbol(details.contract.localSymbol),
asset_class=AssetClass.COMMODITY,
quote_currency=Currency.from_str(details.contract.currency),
price_precision=price_precision,
size_precision=size_precision,
price_increment=Price(details.minTick, price_precision),
size_increment=Quantity(details.sizeIncrement, size_precision),
lot_size=None,
max_quantity=None,
min_quantity=None,
max_notional=None,
min_notional=None,
max_price=None,
min_price=None,
margin_init=Decimal(0),
margin_maint=Decimal(0),
maker_fee=Decimal(0),
taker_fee=Decimal(0),
ts_event=timestamp,
ts_init=timestamp,
info=contract_details_to_dict(details),
)


def decade_digit(last_digit: str, contract: IBContract) -> int:
if year := contract.lastTradeDateOrContractMonth[:4]:
return int(year[2:3])
Expand All @@ -341,8 +449,15 @@ def ib_contract_to_instrument_id(


def ib_contract_to_instrument_id_strict_symbology(contract: IBContract) -> InstrumentId:
symbol = f"{contract.localSymbol}={contract.secType}"
venue = (contract.primaryExchange or contract.exchange).replace(".", "/")
if contract.secType == "CFD":
symbol = f"{contract.localSymbol}={contract.secType}"
venue = "IBCFD"
elif contract.secType == "CMDTY":
symbol = f"{contract.localSymbol}={contract.secType}"
venue = "IBCMDTY"
else:
symbol = f"{contract.localSymbol}={contract.secType}"
venue = (contract.primaryExchange or contract.exchange).replace("/", ".")
return InstrumentId.from_str(f"{symbol}.{venue}")


Expand Down Expand Up @@ -372,6 +487,19 @@ def ib_contract_to_instrument_id_simplified_symbology(contract: IBContract) -> I
f"{contract.localSymbol}".replace(".", "/") or f"{contract.symbol}/{contract.currency}"
)
venue = contract.exchange
elif security_type == "CFD":
if m := RE_CFD_CASH.match(contract.localSymbol):
symbol = (
f"{contract.localSymbol}".replace(".", "/")
or f"{contract.symbol}/{contract.currency}"
)
venue = "IBCFD"
else:
symbol = (contract.symbol).replace(" ", "-")
venue = "IBCFD"
elif security_type == "CMDTY":
symbol = (contract.symbol).replace(" ", "-")
venue = "IBCMDTY"
else:
symbol = None
venue = None
Expand Down Expand Up @@ -402,6 +530,18 @@ def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -
primaryExchange=exchange,
localSymbol=local_symbol,
)
elif security_type == "CFD":
return IBContract(
secType=security_type,
exchange="SMART",
localSymbol=local_symbol, # by IB is a cfd's local symbol of STK with a "n" as tail, e.g. "NVDAn". "
)
elif security_type == "CMDTY":
return IBContract(
secType=security_type,
exchange="SMART",
localSymbol=local_symbol,
)
else:
return IBContract(
secType=security_type,
Expand Down Expand Up @@ -464,6 +604,26 @@ def instrument_id_to_ib_contract_simplified_symbology(instrument_id: InstrumentI
)
else:
raise ValueError(f"Cannot parse {instrument_id}, use 2-digit year for FUT and FOP")
elif instrument_id.venue.value in VENUES_CFD:
if m := RE_CASH.match(instrument_id.symbol.value):
return IBContract(
secType="CFD",
exchange="SMART",
symbol=m["symbol"],
localSymbol=f"{m['symbol']}.{m['currency']}",
)
else:
return IBContract(
secType="CFD",
exchange="SMART",
symbol=f"{instrument_id.symbol.value}".replace("-", " "),
)
elif instrument_id.venue.value in VENUES_CMDTY:
return IBContract(
secType="CMDTY",
exchange="SMART",
symbol=f"{instrument_id.symbol.value}".replace("-", " "),
)
elif instrument_id.venue.value == "InteractiveBrokers": # keep until a better approach
# This will allow to make Instrument request using IBContract from within Strategy
# and depending on the Strategy requirement
Expand Down
4 changes: 4 additions & 0 deletions nautilus_trader/model/instruments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from nautilus_trader.model.instruments.base import Instrument
from nautilus_trader.model.instruments.base import instruments_from_pyo3
from nautilus_trader.model.instruments.betting import BettingInstrument
from nautilus_trader.model.instruments.cfd import Cfd
from nautilus_trader.model.instruments.commodity import Commodity
from nautilus_trader.model.instruments.crypto_future import CryptoFuture
from nautilus_trader.model.instruments.crypto_perpetual import CryptoPerpetual
from nautilus_trader.model.instruments.currency_pair import CurrencyPair
Expand All @@ -42,6 +44,8 @@
"FuturesSpread",
"OptionsContract",
"OptionsSpread",
"Cfd",
"Commodity",
"SyntheticInstrument",
"instruments_from_pyo3",
]
30 changes: 30 additions & 0 deletions nautilus_trader/model/instruments/cfd.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -------------------------------------------------------------------------------------------------
# Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
# https://nautechsystems.io
#
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -------------------------------------------------------------------------------------------------

from nautilus_trader.model.instruments.base cimport Instrument


cdef class Cfd(Instrument):
cdef readonly str isin
"""The instruments International Securities Identification Number (ISIN).\n\n:returns: `str` or ``None``"""

@staticmethod
cdef Cfd from_dict_c(dict values)

@staticmethod
cdef dict to_dict_c(Cfd obj)

@staticmethod
cdef Cfd from_pyo3_c(pyo3_instrument)
Loading

0 comments on commit 1dae661

Please sign in to comment.