From 946d9942e6e3aa61d9dbcff09cbd3bb74ea160b9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Dec 2023 09:50:16 +1100 Subject: [PATCH] Continue Databento integration --- nautilus_trader/adapters/databento/enums.py | 22 ++ nautilus_trader/adapters/databento/loaders.py | 2 + nautilus_trader/adapters/databento/parsing.py | 52 +++ nautilus_trader/adapters/databento/types.py | 354 ++++++++++++++++++ nautilus_trader/test_kit/stubs/identifiers.py | 4 + .../adapters/binance/test_core_types.py | 4 +- .../adapters/databento/test_loaders.py | 28 ++ .../adapters/databento/test_types.py | 210 +++++++++++ tests/test_data/databento/imbalance.dbn.zst | Bin 0 -> 230 bytes tests/test_data/databento/statistics.dbn.zst | Bin 0 -> 203 bytes 10 files changed, 674 insertions(+), 2 deletions(-) create mode 100644 tests/integration_tests/adapters/databento/test_types.py create mode 100644 tests/test_data/databento/imbalance.dbn.zst create mode 100644 tests/test_data/databento/statistics.dbn.zst diff --git a/nautilus_trader/adapters/databento/enums.py b/nautilus_trader/adapters/databento/enums.py index 444ebf8300ee..ea9b9a427eab 100644 --- a/nautilus_trader/adapters/databento/enums.py +++ b/nautilus_trader/adapters/databento/enums.py @@ -28,3 +28,25 @@ class DatabentoInstrumentClass(Enum): FUTURE_SPREAD = "S" OPTION_SPREAD = "T" FX_SPOT = "X" + + +@unique +class DatabentoStatisticType(Enum): + OPENING_PRICE = 1 + INDICATIVE_OPENING_PRICE = 2 + SETTLEMENT_PRICE = 3 + TRADING_SESSION_LOW_PRICE = 4 + TRADING_SESSION_HIGH_PRICE = 5 + CLEARED_VOLUME = 6 + LOWEST_OFFER = 7 + HIGHEST_BID = 8 + OPEN_INTEREST = 9 + FIXING_PRICE = 10 + CLOSE_PRICE = 11 + NET_CHANGE = 12 + + +@unique +class DatabentoStatisticUpdateAction(Enum): + ADDED = 1 + DELETED = 2 diff --git a/nautilus_trader/adapters/databento/loaders.py b/nautilus_trader/adapters/databento/loaders.py index 2ceabeda44cc..e7c3fe13250b 100644 --- a/nautilus_trader/adapters/databento/loaders.py +++ b/nautilus_trader/adapters/databento/loaders.py @@ -45,6 +45,8 @@ class DatabentoDataLoader: - OHLCV_1H -> `Bar` - OHLCV_1D -> `Bar` - DEFINITION -> `Instrument` + - IMBALANCE -> `DatabentoImbalance` + - STATISTICS -> `DatabentoStatistics` For the loader to work correctly, you must first either: - Load Databento instrument definitions from a DBN file using `load_instruments(...)` diff --git a/nautilus_trader/adapters/databento/parsing.py b/nautilus_trader/adapters/databento/parsing.py index 4187e5f942f9..f5069892c145 100644 --- a/nautilus_trader/adapters/databento/parsing.py +++ b/nautilus_trader/adapters/databento/parsing.py @@ -19,7 +19,11 @@ from nautilus_trader.adapters.databento.common import nautilus_instrument_id_from_databento from nautilus_trader.adapters.databento.enums import DatabentoInstrumentClass +from nautilus_trader.adapters.databento.enums import DatabentoStatisticType +from nautilus_trader.adapters.databento.enums import DatabentoStatisticUpdateAction +from nautilus_trader.adapters.databento.types import DatabentoImbalance from nautilus_trader.adapters.databento.types import DatabentoPublisher +from nautilus_trader.adapters.databento.types import DatabentoStatistics from nautilus_trader.core.data import Data from nautilus_trader.core.datetime import secs_to_nanos from nautilus_trader.model.currencies import USD @@ -405,6 +409,50 @@ def parse_ohlcv_msg( ) +def parse_imbalance_msg( + record: databento.ImbalanceMsg, + instrument_id: InstrumentId, + ts_init: int, +) -> TradeTick: + return DatabentoImbalance( + instrument_id=instrument_id, + ref_price=Price.from_raw(record.ref_price, USD.precision), + cont_book_clr_price=Price.from_raw(record.cont_book_clr_price, USD.precision), + auct_interest_clr_price=Price.from_raw(record.auct_interest_clr_price, USD.precision), + paired_qty=Quantity.from_int(record.paired_qty), # Always ints for now + total_imbalance_qty=Quantity.from_int(record.total_imbalance_qty), # Always ints for now + side=parse_order_side(record.side), + significant_imbalance=record.significant_imbalance, + ts_event=record.ts_recv, # More accurate and reliable timestamp + ts_init=ts_init, + ) + + +def parse_statistics_msg( + record: databento.StatMsg, + instrument_id: InstrumentId, + ts_init: int, +) -> TradeTick: + return DatabentoStatistics( + instrument_id=instrument_id, + stat_type=DatabentoStatisticType(record.stat_type), + update_action=DatabentoStatisticUpdateAction(record.update_action), + price=Price.from_raw(record.price, USD.precision) + if record.price is not (2 * 63 - 1) # TODO: Define a constant for this + else None, + quantity=Quantity.from_raw(record.quantity, USD.precision) + if record.quantity is not (2 * 31 - 1) # TODO: Define a constant for this + else None, + channel_id=record.channel_id, + stat_flags=record.stat_flags, + sequence=record.sequence, + ts_ref=record.ts_ref, + ts_in_delta=record.ts_in_delta, + ts_event=record.ts_recv, # More accurate and reliable timestamp + ts_init=ts_init, + ) + + def parse_record_with_metadata( record: databento.DBNRecord, publishers: dict[int, DatabentoPublisher], @@ -452,6 +500,10 @@ def parse_record( return parse_trade_msg(record, instrument_id, ts_init) elif isinstance(record, databento.OHLCVMsg): return parse_ohlcv_msg(record, instrument_id, ts_init) + elif isinstance(record, databento.ImbalanceMsg): + return parse_imbalance_msg(record, instrument_id, ts_init) + elif isinstance(record, databento.StatMsg): + return parse_statistics_msg(record, instrument_id, ts_init) else: raise ValueError( f"Schema {type(record).__name__} is currently unsupported by NautilusTrader", diff --git a/nautilus_trader/adapters/databento/types.py b/nautilus_trader/adapters/databento/types.py index eef786036262..e91669ee8fd5 100644 --- a/nautilus_trader/adapters/databento/types.py +++ b/nautilus_trader/adapters/databento/types.py @@ -13,8 +13,22 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from __future__ import annotations + +from typing import Any + import msgspec +from nautilus_trader.adapters.databento.enums import DatabentoStatisticType +from nautilus_trader.adapters.databento.enums import DatabentoStatisticUpdateAction +from nautilus_trader.core.data import Data +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import order_side_from_str +from nautilus_trader.model.enums import order_side_to_str +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity + Dataset = str PublisherId = int @@ -29,3 +43,343 @@ class DatabentoPublisher(msgspec.Struct, frozen=True): dataset: str venue: str description: str + + +class DatabentoImbalance(Data): + """ + Represents an auction imbalance. + + This data type includes the populated data fields provided by `Databento`, except for + the `publisher_id` and `instrument_id` integers. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the imbalance data. + ref_price : Price + The reference price at which the imbalance shares are calculated. + cont_book_clr_price : Price + The hypothetical auction-clearing price for both cross and continuous orders. + auct_interest_clr_price : Price + The hypothetical auction-clearing price for cross orders only. + paired_qty : Quantity + The quantity of shares which are eligible to be matched at `ref_price`. + total_imbalance_qty : Quantity + The quantity of shares which are not paired at `ref_price`. + side : OrderSide + The market side of the `total_imbalance_qty` (can be `NO_ORDER_SIDE`). + significant_imbalance : str + A venue-specific character code. For Nasdaq, contains the raw Price Variation Indicator. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the data event occurred (Databento `ts_recv`). + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the data object was initialized. + + References + ---------- + https://docs.databento.com/knowledge-base/new-users/fields-by-schema/imbalance-imbalance + + """ + + def __init__( + self, + instrument_id: InstrumentId, + ref_price: Price, + cont_book_clr_price: Price, + auct_interest_clr_price: Price, + paired_qty: Quantity, + total_imbalance_qty: Quantity, + side: OrderSide, + significant_imbalance: str, + ts_event: int, + ts_init: int, + ) -> None: + self.instrument_id = instrument_id + self.ref_price = ref_price + self.cont_book_clr_price = cont_book_clr_price + self.auct_interest_clr_price = auct_interest_clr_price + self.paired_qty = paired_qty + self.total_imbalance_qty = total_imbalance_qty + self.side = side + self.significant_imbalance = significant_imbalance + self._ts_event = ts_event # Required for `Data` base class + self._ts_init = ts_init # Required for `Data` base class + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DatabentoImbalance): + return False + return self.instrument_id == other.instrument_id and self.ts_event == other.ts_event + + def __hash__(self) -> int: + return hash((self.instrument_id, self.ts_event)) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"ref_price={self.ref_price}, " + f"cont_book_clr_price={self.cont_book_clr_price}, " + f"auct_interest_clr_price={self.auct_interest_clr_price}, " + f"paired_qty={self.paired_qty}, " + f"total_imbalance_qty={self.total_imbalance_qty}, " + f"side={order_side_to_str(self.side)}, " + f"significant_imbalance={self.significant_imbalance}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the data event occurred (Databento + `ts_recv`). + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + + @staticmethod + def from_dict(values: dict[str, Any]) -> DatabentoImbalance: + """ + Return `DatabentoImbalance` parsed from the given values. + + Parameters + ---------- + values : dict[str, Any] + The values for initialization. + + Returns + ------- + DatabentoImbalance + + """ + return DatabentoImbalance( + instrument_id=InstrumentId.from_str(values["instrument_id"]), + ref_price=Price.from_str(values["ref_price"]), + cont_book_clr_price=Price.from_str(values["cont_book_clr_price"]), + auct_interest_clr_price=Price.from_str(values["auct_interest_clr_price"]), + paired_qty=Quantity.from_str(values["paired_qty"]), + total_imbalance_qty=Quantity.from_str(values["total_imbalance_qty"]), + side=order_side_from_str(values["side"]), + significant_imbalance=values["significant_imbalance"], + ts_event=values["ts_event"], + ts_init=values["ts_init"], + ) + + @staticmethod + def to_dict(obj: DatabentoImbalance) -> dict[str, Any]: + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, Any] + + """ + return { + "type": type(obj).__name__, + "instrument_id": obj.instrument_id.value, + "ref_price": str(obj.ref_price), + "cont_book_clr_price": str(obj.cont_book_clr_price), + "auct_interest_clr_price": str(obj.auct_interest_clr_price), + "paired_qty": str(obj.paired_qty), + "total_imbalance_qty": str(obj.total_imbalance_qty), + "side": order_side_to_str(obj.side), + "significant_imbalance": obj.significant_imbalance, + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + } + + +class DatabentoStatistics(Data): + """ + Represents a statistics message. + + This data type includes the populated data fields provided by `Databento`, except for + the `publisher_id` and `instrument_id` integers. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the statistics message. + stat_type : DatabentoStatisticType + The type of statistic value contained in the message. + update_action : DatabentoStatisticUpdateAction + Indicates if the statistic is newly added (1) or deleted (2). + (Deleted is only used with some stat_types). + price : Price, optional + The statistics price. + quantity : Quantity, optional + The value for non-price statistics. + channel_id : int + The channel ID within the venue. + stat_flags : int + Additional flags associated with certain stat types. + sequence : int + The message sequence number assigned at the venue. + ts_ref : uint64_t + The UNIX timestamp (nanoseconds) Databento `ts_ref` reference timestamp). + ts_in_delta : int32_t + The matching-engine-sending timestamp expressed as the number of nanoseconds before the Databento `ts_recv`. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the data event occurred (Databento `ts_recv`). + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the data object was initialized. + + References + ---------- + https://docs.databento.com/knowledge-base/new-users/fields-by-schema/statistics-statistics + + """ + + def __init__( + self, + instrument_id: InstrumentId, + stat_type: DatabentoStatisticType, + update_action: DatabentoStatisticUpdateAction, + price: Price | None, + quantity: Quantity | None, + channel_id: int, + stat_flags: int, + sequence: int, + ts_ref: int, + ts_in_delta: int, + ts_event: int, + ts_init: int, + ) -> None: + self.instrument_id = instrument_id + self.stat_type = stat_type + self.update_action = update_action + self.price = price + self.quantity = quantity + self.channel_id = channel_id + self.stat_flags = stat_flags + self.sequence = sequence + self.ts_ref = ts_ref + self.ts_in_delta = ts_in_delta + self._ts_event = ts_event # Required for `Data` base class + self._ts_init = ts_init # Required for `Data` base class + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DatabentoStatistics): + return False + return self.instrument_id == other.instrument_id and self.ts_event == other.ts_event + + def __hash__(self) -> int: + return hash((self.instrument_id, self.ts_event)) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"stat_type={self.stat_type}, " + f"update_action={self.update_action}, " + f"price={self.price}, " + f"quantity={self.quantity}, " + f"channel_id={self.channel_id}, " + f"stat_flags={self.stat_flags}, " + f"sequence={self.sequence}, " + f"ts_ref={self.ts_ref}, " + f"ts_in_delta={self.ts_in_delta}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the data event occurred (Databento + `ts_recv`). + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + + @staticmethod + def from_dict(values: dict[str, Any]) -> DatabentoStatistics: + """ + Return `DatabentoStatistics` parsed from the given values. + + Parameters + ---------- + values : dict[str, Any] + The values for initialization. + + Returns + ------- + DatabentoStatistics + + """ + price: str | None = values["price"] + quantity: str | None = values["quantity"] + + return DatabentoStatistics( + instrument_id=InstrumentId.from_str(values["instrument_id"]), + stat_type=DatabentoStatisticType(values["stat_type"]), + update_action=DatabentoStatisticUpdateAction(values["update_action"]), + price=Price.from_str(price) if price is not None else None, + quantity=Quantity.from_str(quantity) if quantity is not None else None, + channel_id=values["channel_id"], + stat_flags=values["stat_flags"], + sequence=values["sequence"], + ts_ref=values["ts_ref"], + ts_in_delta=values["ts_in_delta"], + ts_event=values["ts_event"], + ts_init=values["ts_init"], + ) + + @staticmethod + def to_dict(obj: DatabentoStatistics) -> dict[str, Any]: + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, Any] + + """ + return { + "type": type(obj).__name__, + "instrument_id": obj.instrument_id.value, + "stat_type": obj.stat_type.value, + "update_action": obj.update_action.value, + "price": str(obj.price) if obj.price is not None else None, + "quantity": str(obj.quantity) if obj.quantity is not None else None, + "channel_id": obj.channel_id, + "stat_flags": obj.stat_flags, + "sequence": obj.sequence, + "ts_ref": obj.ts_ref, + "ts_in_delta": obj.ts_in_delta, + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + } diff --git a/nautilus_trader/test_kit/stubs/identifiers.py b/nautilus_trader/test_kit/stubs/identifiers.py index 425fe31cd10c..a74ab1a311ad 100644 --- a/nautilus_trader/test_kit/stubs/identifiers.py +++ b/nautilus_trader/test_kit/stubs/identifiers.py @@ -76,6 +76,10 @@ def usdjpy_id() -> InstrumentId: def audusd_idealpro_id() -> InstrumentId: return InstrumentId(Symbol("AUD/USD"), Venue("IDEALPRO")) + @staticmethod + def msft_xnas_id() -> InstrumentId: + return InstrumentId(Symbol("MSFT"), Venue("XNAS")) + @staticmethod def betting_instrument_id(): from nautilus_trader.adapters.betfair.parsing.common import betfair_instrument_id diff --git a/tests/integration_tests/adapters/binance/test_core_types.py b/tests/integration_tests/adapters/binance/test_core_types.py index 1873bec22c1d..e8056ccf14a7 100644 --- a/tests/integration_tests/adapters/binance/test_core_types.py +++ b/tests/integration_tests/adapters/binance/test_core_types.py @@ -133,7 +133,7 @@ def test_binance_ticker_to_from_dict(): values = ticker.to_dict(ticker) # Assert - BinanceTicker.from_dict(values) + assert BinanceTicker.from_dict(values) == ticker assert values == { "type": "BinanceTicker", "instrument_id": "BTCUSDT.BINANCE", @@ -213,7 +213,7 @@ def test_binance_bar_to_from_dict(): values = bar.to_dict(bar) # Assert - BinanceBar.from_dict(values) + assert BinanceBar.from_dict(values) == bar assert values == { "type": "BinanceBar", "bar_type": "BTCUSDT.BINANCE-1-MINUTE-LAST-EXTERNAL", diff --git a/tests/integration_tests/adapters/databento/test_loaders.py b/tests/integration_tests/adapters/databento/test_loaders.py index 1baec6a2de5f..e73d63577f24 100644 --- a/tests/integration_tests/adapters/databento/test_loaders.py +++ b/tests/integration_tests/adapters/databento/test_loaders.py @@ -16,6 +16,8 @@ import pytest from nautilus_trader.adapters.databento.loaders import DatabentoDataLoader +from nautilus_trader.adapters.databento.types import DatabentoImbalance +from nautilus_trader.adapters.databento.types import DatabentoStatistics from nautilus_trader.model.currencies import USD from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType @@ -435,3 +437,29 @@ def test_loader_with_ohlcv_1d() -> None: # Assert assert len(data) == 0 # ?? + + +def test_loader_with_imbalance() -> None: + # Arrange + loader = DatabentoDataLoader() + path = DATABENTO_TEST_DATA_DIR / "imbalance.dbn.zst" + + # Act + data = loader.from_dbn(path) + + # Assert + assert len(data) == 4 + assert isinstance(data[0], DatabentoImbalance) + + +def test_loader_with_statistics() -> None: + # Arrange + loader = DatabentoDataLoader() + path = DATABENTO_TEST_DATA_DIR / "statistics.dbn.zst" + + # Act + data = loader.from_dbn(path) + + # Assert + assert len(data) == 4 + assert isinstance(data[0], DatabentoStatistics) diff --git a/tests/integration_tests/adapters/databento/test_types.py b/tests/integration_tests/adapters/databento/test_types.py new file mode 100644 index 000000000000..db7c96c5a394 --- /dev/null +++ b/tests/integration_tests/adapters/databento/test_types.py @@ -0,0 +1,210 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 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. +# ------------------------------------------------------------------------------------------------- + +import pickle + +import pandas as pd + +from nautilus_trader.adapters.databento.enums import DatabentoStatisticType +from nautilus_trader.adapters.databento.enums import DatabentoStatisticUpdateAction +from nautilus_trader.adapters.databento.types import DatabentoImbalance +from nautilus_trader.adapters.databento.types import DatabentoStatistics +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity +from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs + + +def test_imbalance_hash_str_repr() -> None: + # Arrange + imbalance = DatabentoImbalance( + instrument_id=TestIdStubs.msft_xnas_id(), + ref_price=Price.from_str("238.94"), + cont_book_clr_price=Price.from_str("238.94"), + auct_interest_clr_price=Price.from_str("238.94"), + paired_qty=Quantity.from_int(242735), + total_imbalance_qty=Quantity.from_int(1248), + side=OrderSide.BUY, + significant_imbalance="L", + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act, Assert + assert ( + str(imbalance) + == "DatabentoImbalance(instrument_id=MSFT.XNAS, ref_price=238.94, cont_book_clr_price=238.94, auct_interest_clr_price=238.94, paired_qty=242735, total_imbalance_qty=1248, side=BUY, significant_imbalance=L, ts_event=1664458080039784528, ts_init=1664458080039774464)" # noqa + ) + assert ( + repr(imbalance) + == "DatabentoImbalance(instrument_id=MSFT.XNAS, ref_price=238.94, cont_book_clr_price=238.94, auct_interest_clr_price=238.94, paired_qty=242735, total_imbalance_qty=1248, side=BUY, significant_imbalance=L, ts_event=1664458080039784528, ts_init=1664458080039774464)" # noqa + ) + assert isinstance(hash(imbalance), int) + + +def test_imbalance_pickling() -> None: + # Arrange + imbalance = DatabentoImbalance( + instrument_id=TestIdStubs.msft_xnas_id(), + ref_price=Price.from_str("238.94"), + cont_book_clr_price=Price.from_str("238.94"), + auct_interest_clr_price=Price.from_str("238.94"), + paired_qty=Quantity.from_int(242735), + total_imbalance_qty=Quantity.from_int(1248), + side=OrderSide.BUY, + significant_imbalance="L", + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act + pickled = pickle.dumps(imbalance) + unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) + + # Assert + assert unpickled == imbalance + assert ( + repr(unpickled) + == "DatabentoImbalance(instrument_id=MSFT.XNAS, ref_price=238.94, cont_book_clr_price=238.94, auct_interest_clr_price=238.94, paired_qty=242735, total_imbalance_qty=1248, side=BUY, significant_imbalance=L, ts_event=1664458080039784528, ts_init=1664458080039774464)" # noqa + ) + + +def test_to_dict_from_dict_round_trip() -> None: + # Arrange + imbalance = DatabentoImbalance( + instrument_id=TestIdStubs.msft_xnas_id(), + ref_price=Price.from_str("238.94"), + cont_book_clr_price=Price.from_str("238.94"), + auct_interest_clr_price=Price.from_str("238.94"), + paired_qty=Quantity.from_int(242735), + total_imbalance_qty=Quantity.from_int(1248), + side=OrderSide.BUY, + significant_imbalance="L", + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act + values = imbalance.to_dict(imbalance) + + # Assert + assert DatabentoImbalance.from_dict(values) == imbalance + assert values == { + "type": "DatabentoImbalance", + "instrument_id": "MSFT.XNAS", + "ref_price": "238.94", + "cont_book_clr_price": "238.94", + "auct_interest_clr_price": "238.94", + "paired_qty": "242735", + "total_imbalance_qty": "1248", + "side": "BUY", + "significant_imbalance": "L", + "ts_event": 1664458080039784528, + "ts_init": 1664458080039774464, + } + + +def test_statistics_hash_str_repr() -> None: + # Arrange + statistics = DatabentoStatistics( + instrument_id=InstrumentId.from_str("TSLA 230901C00250000.XBOX"), + stat_type=DatabentoStatisticType.TRADING_SESSION_HIGH_PRICE, + update_action=DatabentoStatisticUpdateAction.ADDED, + price=Price.from_str("3.450000000"), + quantity=None, + channel_id=41, + stat_flags=0, + sequence=1278617494, + ts_ref=1, + ts_in_delta=2, + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act, Assert + assert ( + str(statistics) + == "DatabentoStatistics(instrument_id=TSLA 230901C00250000.XBOX, stat_type=DatabentoStatisticType.TRADING_SESSION_HIGH_PRICE, update_action=DatabentoStatisticUpdateAction.ADDED, price=3.450000000, quantity=None, channel_id=41, stat_flags=0, sequence=1278617494, ts_ref=1, ts_in_delta=2, ts_event=1664458080039784528, ts_init=1664458080039774464)" # noqa + ) + assert ( + repr(statistics) + == "DatabentoStatistics(instrument_id=TSLA 230901C00250000.XBOX, stat_type=DatabentoStatisticType.TRADING_SESSION_HIGH_PRICE, update_action=DatabentoStatisticUpdateAction.ADDED, price=3.450000000, quantity=None, channel_id=41, stat_flags=0, sequence=1278617494, ts_ref=1, ts_in_delta=2, ts_event=1664458080039784528, ts_init=1664458080039774464)" # noqa + ) + assert isinstance(hash(statistics), int) + + +def test_statistics_pickle() -> None: + # Arrange + statistics = DatabentoStatistics( + instrument_id=InstrumentId.from_str("TSLA 230901C00250000.XBOX"), + stat_type=DatabentoStatisticType.TRADING_SESSION_HIGH_PRICE, + update_action=DatabentoStatisticUpdateAction.ADDED, + price=Price.from_str("3.450000000"), + quantity=None, + channel_id=41, + stat_flags=0, + sequence=1278617494, + ts_ref=1, + ts_in_delta=2, + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act + pickled = pickle.dumps(statistics) + unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) + + # Assert + assert unpickled == statistics + + +def test_statistics_to_dict_from_dict_round_trip() -> None: + # Arrange + statistics = DatabentoStatistics( + instrument_id=InstrumentId.from_str("TSLA 230901C00250000.XBOX"), + stat_type=DatabentoStatisticType.TRADING_SESSION_HIGH_PRICE, + update_action=DatabentoStatisticUpdateAction.ADDED, + price=Price.from_str("3.450000000"), + quantity=None, + channel_id=41, + stat_flags=0, + sequence=1278617494, + ts_ref=1, + ts_in_delta=2, + ts_event=pd.Timestamp("2022-09-29T13:28:00.039784528Z").value, + ts_init=pd.Timestamp("2022-09-29T13:28:00.039774464Z").value, + ) + + # Act + values = statistics.to_dict(statistics) + + # Assert + assert DatabentoStatistics.from_dict(values) == statistics + assert values == { + "type": "DatabentoStatistics", + "instrument_id": "TSLA 230901C00250000.XBOX", + "stat_type": 5, + "update_action": 1, + "price": "3.450000000", + "quantity": None, + "channel_id": 41, + "stat_flags": 0, + "sequence": 1278617494, + "ts_ref": 1, + "ts_in_delta": 2, + "ts_event": 1664458080039784528, + "ts_init": 1664458080039774464, + } diff --git a/tests/test_data/databento/imbalance.dbn.zst b/tests/test_data/databento/imbalance.dbn.zst new file mode 100644 index 0000000000000000000000000000000000000000..d90b32519d06f87872d969f7e08f244d084d1056 GIT binary patch literal 230 zcmV6YR2BdLkHp^w zRTcyQ{{a9_RzyJzKQsXiKQsY0F*P*`0Du6n+0%leYygb1VnM(`;240f0oVaT;el=_ zwJ-f-zyQSq07L{F6aoMX7ytnJoaY*;=TsKP!hqiesCfXUio<2T~K@40#zRwb!oB(kKhS{bEcLj*E zF#Km^aC8jtQBW{4HZV3ebT%+BG&MIhFxaudgmEVj8yT7yaWU-VUA5?qq(h(>%c;mn zg$D`+JO@rQl|LwQkJSKLnZm#j%EWMmNkBrLA&vuR$qVs6eSHDq*51

yN54Fu2q* zZ~}!H83ZJxf4D@uD!Qq-^Drm~GFW{uWDxogn;>W)eW)NoVTY1mm#~+_cgBk20InrD A`Tzg` literal 0 HcmV?d00001