diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9dbcff8..5678c9d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,10 @@ -# Pull Request +# Pull request Please include a summary of the changes. +Please also include relevant motivation and context. +List any dependencies that are required for this change. + +Fixes # (issue) ## Type of change @@ -13,4 +17,22 @@ Please delete options that are not relevant. ## How has this change been tested? -Please describe how this code was/is tested. +Please describe the tests that you ran to verify your changes. +Provide instructions so we can reproduce. +Please also list any relevant details for your test configuration. + +- [ ] Test A +- [ ] Test B + +## Checklist + +- [ ] My code builds locally with no new warnings (`scripts/build.sh`) +- [ ] My code follows the style guidelines (`scripts/lint.sh` and `scripts/format.sh`) +- [ ] New and existing unit tests pass locally with my changes (`scripts/test.sh`) +- [ ] I have made corresponding changes to the documentation +- [ ] I have added tests that prove my fix is effective or that my feature works + +## Declaration + +I confirm this contribution is made under an Apache 2.0 license and that I have the authority +necessary to make this contribution on behalf of its copyright owner. diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7756b72..3715857 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,7 +2,9 @@ name: build # Build and test dbn -on: push +on: + pull_request: + push: jobs: x86_64-build: diff --git a/CHANGELOG.md b/CHANGELOG.md index d1cbb62..ace06fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 0.17.0 - 2024-04-01 + +### Enhancements +- Added new record type `CbboMsg`, new rtypes and schema types for `Cbbo`, `Cbbo1s`, + `Cbbo1m`, `Tcbbo`, `Bbo1s`, and `Bbo1m` +- Added `Volatility` and `Delta` `StatType` variants +- Added `Undefined` and `TimeProRata` `MatchAlgorithm` variants +- Exported more enums to Python: + - `Action` + - `InstrumentClass` + - `MatchAlgorithm` + - `SecurityUpdateAction` + - `Side` + - `StatType` + - `StatUpdateAction` + - `StatusAction` + - `StatusReason` + - `TradingEvent` + - `TriState` + - `UserDefinedInstrument` + +### Breaking changes +- Removed `Default` trait implementation for `Mbp1Msg` due to it now having multiple + permissible `rtype` values. Users should use `default_for_schema` instead +- Changed the default `match_algorithm` for `InstrumentDefMsg` and `InstrumentDefMsgV1` + from `Fifo` to `Undefined` +- Made `Dataset`, `Venue`, and `Publisher` non-exhaustive to allow future additions + without breaking changes +- Renamed publishers from deprecated datasets to their respective sources (`XNAS.NLS` + and `XNYS.TRADES` respectively) + +### Deprecations +- Deprecated dataset values `FINN.NLS` and `FINY.TRADES` + +### Bug fixes +- Fixed an issue where the Python `MappingIntervalDict` was not exported +- Fixed Python type stubs for `VersionUpgradePolicy` and `SType` + ## 0.16.0 - 2024-03-01 ### Enhancements - Updated `StatusMsg` and made it public in preparation for releasing a status schema @@ -8,7 +46,7 @@ - Added `-t` and `--tsv` flags to DBN CLI to encode tab-separated values (TSV) - Added `delimiter` method to builders for `DynEncoder` and `CsvEncoder` to customize the field delimiter character, allowing DBN to be encoded as tab-separated values (TSV) -- Document cancellation safety for `AsyncRecordDecoder::decode_ref` (credit: @yongqli) +- Documented cancellation safety for `AsyncRecordDecoder::decode_ref` (credit: @yongqli) - Added new publisher values for consolidated DBEQ.MAX - Added C FFI conversion functions from `ErrorMsgV1` to `ErrorMsg` and `SystemMsgV1` to `SystemMsg` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 47287ee..99abe49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,4 @@ -We welcome feedback through discussions and issues on GitHub, however we don't currently accept pull requests due to the open-source repository being a downstream mirror of our internal codebase. - -Please direct email feedback to support@databento.com or carter@databento.com. +Thank you for taking the time to contribute to our project. +We welcome feedback through discussions and issues on GitHub, as well as our [community Slack](https://databento.com/support). +While we don't merge pull requests directly due to the open-source repository being a downstream +mirror of our internal codebase, we can commit the changes upstream with the original author. diff --git a/Cargo.lock b/Cargo.lock index 44b37da..1ed439d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,7 +271,7 @@ dependencies = [ [[package]] name = "databento-dbn" -version = "0.16.0" +version = "0.17.0" dependencies = [ "dbn", "pyo3", @@ -282,7 +282,7 @@ dependencies = [ [[package]] name = "dbn" -version = "0.16.0" +version = "0.17.0" dependencies = [ "async-compression", "csv", @@ -304,7 +304,7 @@ dependencies = [ [[package]] name = "dbn-c" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "cbindgen", @@ -314,7 +314,7 @@ dependencies = [ [[package]] name = "dbn-cli" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "assert_cmd", @@ -329,7 +329,7 @@ dependencies = [ [[package]] name = "dbn-macros" -version = "0.16.0" +version = "0.17.0" dependencies = [ "csv", "dbn", diff --git a/Cargo.toml b/Cargo.toml index e204365..26fbeb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" [workspace.package] authors = ["Databento "] edition = "2021" -version = "0.16.0" +version = "0.17.0" documentation = "https://docs.databento.com" repository = "https://github.com/databento/dbn" license = "Apache-2.0" diff --git a/python/pyproject.toml b/python/pyproject.toml index 3453a02..43f5ae7 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "databento-dbn" -version = "0.16.0" +version = "0.17.0" description = "Python bindings for encoding and decoding Databento Binary Encoding (DBN)" authors = ["Databento "] license = "Apache-2.0" @@ -17,7 +17,7 @@ build-backend = "maturin" [project] name = "databento-dbn" -version = "0.16.0" +version = "0.17.0" authors = [ { name = "Databento", email = "support@databento.com" } ] diff --git a/python/python/databento_dbn/__init__.py b/python/python/databento_dbn/__init__.py index ffe6582..ff3f65a 100644 --- a/python/python/databento_dbn/__init__.py +++ b/python/python/databento_dbn/__init__.py @@ -1,5 +1,7 @@ import datetime as dt -from typing import Protocol, Sequence +from typing import Protocol +from typing import Sequence +from typing import TypedDict # Import native module from ._lib import * # noqa: F403 @@ -24,6 +26,27 @@ class MappingInterval(Protocol): end_date: dt.date symbol: str + +class MappingIntervalDict(TypedDict): + """ + Represents a symbol mapping over a start and end date range interval. + + Parameters + ---------- + start_date : dt.date + The start of the mapping period. + end_date : dt.date + The end of the mapping period. + symbol : str + The symbol value. + + """ + + start_date: dt.date + end_date: dt.date + symbol: str + + class SymbolMapping(Protocol): """ Represents the mappings for one native symbol. diff --git a/python/python/databento_dbn/_lib.pyi b/python/python/databento_dbn/_lib.pyi index 08d9d55..4f3a922 100644 --- a/python/python/databento_dbn/_lib.pyi +++ b/python/python/databento_dbn/_lib.pyi @@ -5,11 +5,17 @@ import datetime as dt from collections.abc import Iterable from collections.abc import Sequence from enum import Enum -from typing import BinaryIO, ClassVar, SupportsBytes, TextIO, TypedDict, Union - +from typing import BinaryIO +from typing import ClassVar +from typing import Optional +from typing import SupportsBytes +from typing import TextIO +from typing import Union + +from databento_dbn import MappingIntervalDict from databento_dbn import SymbolMapping - +DBN_VERSION: int FIXED_PRICE_SCALE: int UNDEF_PRICE: int UNDEF_ORDER_SIZE: int @@ -20,6 +26,7 @@ _DBNRecord = Union[ Metadata, MBOMsg, MBP1Msg, + CbboMsg, MBP10Msg, OHLCVMsg, TradeMsg, @@ -35,101 +42,162 @@ _DBNRecord = Union[ StatMsg, ] -class Compression(Enum): +class Side(Enum): """ - Data compression format. + A side of the market. The side of the market for resting orders, or the side + of the aggressor for trades. + ASK + A sell order or sell aggressor in a trade. + BID + A buy order or a buy aggressor in a trade. NONE - Uncompressed - ZSTD - Zstandard compressed. + No side specified by the original source. """ + ASK: str + BID: str NONE: str - ZSTD: str @classmethod - def from_str(cls, value: str) -> Compression: ... + def from_str(cls, value: str) -> Side: ... @classmethod - def variants(cls) -> Iterable[Compression]: ... + def variants(cls) -> Iterable[Side]: ... -class Encoding(Enum): +class Action(Enum): """ - Data output encoding. + A tick action. + + MODIFY + An existing order was modified. + TRADE + A trade executed. + FILL + An existing order was filled. + CANCEL + An order was cancelled. + ADD + A new order was added. + CLEAR + Reset the book; clear all orders for an instrument. - DBN - Databento Binary Encoding. - CSV - Comma-separated values. - JSON - JavaScript object notation. + """ + + MODIFY: str + TRADE: str + FILL: str + CANCEL: str + ADD: str + CLEAR: str + + @classmethod + def from_str(cls, value: str) -> Action: ... + @classmethod + def variants(cls) -> Iterable[Action]: ... +class InstrumentClass(Enum): """ + The class of instrument. + + BOND + A bond. + CALL + A call option. + FUTURE + A future. + STOCK + A stock. + MIXED_SPREAD + A spread composed of multiple instrument classes. + PUT + A put option. + FUTURE_SPREAD + A spread composed of futures. + OPTION_SPREAD + A spread composed of options. + FX_SPOT + A foreign exchange spot. - DBN: str - CSV: str - JSON: str + """ + + BOND: str + CALL: str + FUTURE: str + STOCK: str + MIXED_SPREAD: str + PUT: str + FUTURE_SPREAD: str + OPTION_SPREAD: str + FX_SPOT: str @classmethod - def from_str(cls, value: str) -> Encoding: ... + def from_str(cls, value: str) -> InstrumentClass: ... @classmethod - def variants(cls) -> Iterable[Encoding]: ... + def variants(cls) -> Iterable[InstrumentClass]: ... -class Schema(Enum): +class MatchAlgorithm(Enum): + """ + The type of matching algorithm used for the instrument at the exchange. + + + UNDEFINED + No matching algorithm was specified. + FIFO + First-in-first-out matching. + CONFIGURABLE + A configurable match algorithm. + PRO_RATA + Trade quantity is allocated to resting orders based on a pro-rata percentage: resting order quantity divided by total quantity. + FIFO_LMM + Like `FIFO` but with LMM allocations prior to FIFO allocations. + THRESHOLD_PRO_RATA + Like `PRO_RATA` but includes a configurable allocation to the first order that improves the market. + FIFO_TOP_LMM + Like `FIFO_LMM` but includes a configurable allocation to the first order that improves the market. + THRESHOLD_PRO_RATA_LMM + Like `THRESHOLD_PRO_RATA` but includes a special priority to LMMs. + EURODOLLAR_FUTURES + Special variant used only for Eurodollar futures on CME. + TIME_PRO_RATA + Trade quantity is shared between all orders at the best price. Orders with the + highest time priority receive a higher matched quantity. """ - A DBN record schema. - MBO - Market by order. - MBP_1 - Market by price with a book depth of 1. - MBP_10 - Market by price with a book depth of 10. - TBBO - All trade events with the best bid and offer (BBO) immediately before the effect of the trade. - TRADES - All trade events. - OHLCV_1S - Open, high, low, close, and volume at a one-second interval. - OHLCV_1M - Open, high, low, close, and volume at a one-minute interval. - OHLCV_1H - Open, high, low, close, and volume at an hourly interval. - OHLCV_1D - Open, high, low, close, and volume at a daily interval. - OHLCV_EOD - Open, high, low, close, and volume at a daily cadence based on the end of the trading session. - DEFINITION - Instrument definitions. - STATISTICS - Additional data disseminated by publishers. - STATUS - Exchange status. - IMBALANCE - Auction imbalance events. + UNDEFINED: str + FIFO: str + CONFIGURABLE: str + PRO_RATA: str + FIFO_LMM: str + THRESHOLD_PRO_RATA: str + FIFO_TOP_LMM: str + THRESHOLD_PRO_RATA_LMM: str + EURODOLLAR_FUTURES: str + TIME_PRO_RATA: str + @classmethod + def from_str(cls, value: str) -> MatchAlgorithm: ... + @classmethod + def variants(cls) -> Iterable[MatchAlgorithm]: ... + +class UserDefinedInstrument(Enum): """ + Whether the instrument is user-defined. - MBO: str - MBP_1: str - MBP_10: str - TBBO: str - TRADES: str - OHLCV_1S: str - OHLCV_1M: str - OHLCV_1H: str - OHLCV_1D: str - OHLCV_EOD: str - DEFINITION: str - STATISTICS: str - STATUS: str - IMBALANCE: str + NO + The instrument is not user-defined. + YES + The instrument is user-defined. + + """ + + NO: str + YES: str @classmethod - def from_str(cls, value: str) -> Schema: ... + def from_str(cls, value: str) -> UserDefinedInstrument: ... @classmethod - def variants(cls) -> Iterable[Schema]: ... + def variants(cls) -> Iterable[UserDefinedInstrument]: ... class SType(Enum): """ @@ -146,6 +214,10 @@ class SType(Enum): PARENT A Databento-specific symbology for referring to a group of symbols by one "parent" symbol, e.g. ES.FUT to refer to all ES futures. + NASDAQ + Symbology for US equities using NASDAQ Integrated suffix conventions. + CMS + Symbology for US equities using CMS suffix conventions. """ @@ -153,6 +225,8 @@ class SType(Enum): RAW_SYMBOL: str CONTINUOUS: str PARENT: str + NASDAQ: str + CMS: str @classmethod def from_str(cls, value: str) -> SType: ... @@ -200,6 +274,20 @@ class RType(Enum): Denotes a statistics record from the publisher (not calculated by Databento). MBO Denotes a market by order record. + CBBO + Denotes a consolidated best bid and offer record. + CBBO_1S + Denotes a consolidated best bid and offer record. + CBBO_1M + Denotes a consolidated best bid and offer record subsampled on a one-minute + interval. + TCBBO + Denotes a consolidated best bid and offer trade record containing the + consolidated BBO before the trade. + BBO_1S + Denotes a best bid and offer record subsampled on a one-second interval. + BBO_1M + Denotes a best bid and offer record subsampled on a one-minute interval. """ # noqa: D405 D407 D411 @@ -212,42 +300,462 @@ class RType(Enum): @classmethod def variants(cls) -> Iterable[RType]: ... -class VersionUpgradePolicy(Enum): +class Schema(Enum): """ - How to handle decoding a DBN data from a prior version. + A DBN record schema. - AS_IS - Decode data from previous versions as-is. - UPGRADE - Decode data from previous versions converting it to the latest version. + MBO + Market by order. + MBP_1 + Market by price with a book depth of 1. + MBP_10 + Market by price with a book depth of 10. + TBBO + All trade events with the best bid and offer (BBO) immediately before the effect of the trade. + TRADES + All trade events. + OHLCV_1S + Open, high, low, close, and volume at a one-second interval. + OHLCV_1M + Open, high, low, close, and volume at a one-minute interval. + OHLCV_1H + Open, high, low, close, and volume at an hourly interval. + OHLCV_1D + Open, high, low, close, and volume at a daily interval. + OHLCV_EOD + Open, high, low, close, and volume at a daily cadence based on the end of the trading session. + DEFINITION + Instrument definitions. + STATISTICS + Additional data disseminated by publishers. + STATUS + Exchange status. + IMBALANCE + Auction imbalance events. + CBBO + Consolidated best bid and offer. + CBBO_1S + Consolidated best bid and offer record. + CBBO_1M + Consolidated best bid and offer record subsampled on a one-second interval. + TCBBO + Consolidated best bid and offer record subsampled on a one-minute interval. + BBO_1S + Consolidated best bid and offer trade record containing the consolidated BBO before the trade. + BBO_1M + Best bid and offer record subsampled on a one-second interval. + + """ + + MBO: str + MBP_1: str + MBP_10: str + TBBO: str + TRADES: str + OHLCV_1S: str + OHLCV_1M: str + OHLCV_1H: str + OHLCV_1D: str + OHLCV_EOD: str + DEFINITION: str + STATISTICS: str + STATUS: str + IMBALANCE: str + CBBO: str + CBBO_1S: str + CBBO_1M: str + TCBBO: str + BBO_1S: str + BBO_1M: str + + @classmethod + def from_str(cls, value: str) -> Schema: ... + @classmethod + def variants(cls) -> Iterable[Schema]: ... + +class Encoding(Enum): + """ + Data output encoding. + + DBN + Databento Binary Encoding. + CSV + Comma-separated values. + JSON + JavaScript object notation. """ - AS_IS: str - UPGRADE: str + DBN: str + CSV: str + JSON: str @classmethod - def from_str(cls, value: str) -> SType: ... + def from_str(cls, value: str) -> Encoding: ... @classmethod - def variants(cls) -> Iterable[SType]: ... + def variants(cls) -> Iterable[Encoding]: ... + +class Compression(Enum): + """ + Data compression format. + + NONE + Uncompressed. + ZSTD + Zstandard compressed. -class MappingIntervalDict(TypedDict): """ - Represents a symbol mapping over a start and end date range interval. - Parameters - ---------- - start_date : dt.date - The start of the mapping period. - end_date : dt.date - The end of the mapping period. - symbol : str - The symbol value. + NONE: str + ZSTD: str + + @classmethod + def from_str(cls, value: str) -> Compression: ... + @classmethod + def variants(cls) -> Iterable[Compression]: ... + +class SecurityUpdateAction(Enum): + """ + The type of definition update. + + ADD + A new instrument definition. + MODIFY + A modified instrument definition of an existing one. + DELETE + Removal of an instrument definition. + INVALID + Deprecated + + """ + + ADD: str + MODIFY: str + DELETE: str + INVALID: str + + @classmethod + def from_str(cls, value: str) -> SecurityUpdateAction: ... + @classmethod + def variants(cls) -> Iterable[SecurityUpdateAction]: ... + +class StatType(Enum): + """ + The type of statistic contained in a `StatMsg`. + + OPENING_PRICE + The price of the first trade of an instrument. `price` will be set. + INDICATIVE_OPENING_PRICE + The probable price of the first trade of an instrument published during pre- open. Both + `price` and `quantity` will be set. + SETTLEMENT_PRICE + The settlement price of an instrument. `price` will be set and `flags` indicate whether the + price is final or preliminary and actual or theoretical. `ts_ref` will indicate the trading + date of the settlement price. + TRADING_SESSION_LOW_PRICE + The lowest trade price of an instrument during the trading session. `price` will be set. + TRADING_SESSION_HIGH_PRICE + The highest trade price of an instrument during the trading session. `price` will be set. + CLEARED_VOLUME + The number of contracts cleared for an instrument on the previous trading date. `quantity` + will be set. `ts_ref` will indicate the trading date of the volume. + LOWEST_OFFER + The lowest offer price for an instrument during the trading session. `price` will be set. + HIGHEST_BID + The highest bid price for an instrument during the trading session. `price` will be set. + OPEN_INTEREST + The current number of outstanding contracts of an instrument. `quantity` will be set. + `ts_ref` will indicate the trading date for which the open interest was calculated. + FIXING_PRICE + The volume-weighted average price (VWAP) for a fixing period. `price` will be set. + CLOSE_PRICE + The last trade price during a trading session. `price` will be set. + NET_CHANGE + The change in price from the close price of the previous trading session to the most recent + trading session. `price` will be set. + VWAP + The volume-weighted average price (VWAP) during the trading session. `price` will be set to + the VWAP while `quantity` will be the traded volume. + VOLATILITY + The implied volatility associated with the settlement price. + DELTA + The option delta associated with the settlement price. + + """ + + OPENING_PRICE: int + INDICATIVE_OPENING_PRICE: int + SETTLEMENT_PRICE: int + TRADING_SESSION_LOW_PRICE: int + TRADING_SESSION_HIGH_PRICE: int + CLEARED_VOLUME: int + LOWEST_OFFER: int + HIGHEST_BID: int + OPEN_INTEREST: int + FIXING_PRICE: int + CLOSE_PRICE: int + NET_CHANGE: int + VWAP: int + VOLATILITY: int + DELTA: int + + @classmethod + def variants(cls) -> Iterable[StatType]: ... + +class StatUpdateAction(Enum): """ + The type of `StatMsg` update. - start_date: dt.date - end_date: dt.date - symbol: str + NEW + A new statistic. + DELETE + A removal of a statistic. + + """ + + NEW: str + DELETE: str + + @classmethod + def from_str(cls, value: str) -> StatUpdateAction: ... + @classmethod + def variants(cls) -> Iterable[StatUpdateAction]: ... + +class StatusAction(Enum): + """ + The primary enum for the type of `StatusMsg` update. + + NONE + No change. + PRE_OPEN + The instrument is in a pre-open period. + PRE_CROSS + The instrument is in a pre-cross period. + QUOTING + The instrument is quoting but not trading. + CROSS + The instrument is in a cross/auction. + ROTATION + The instrument is being opened through a trading rotation. + NEW_PRICE_INDICATION + A new price indication is available for the instrument. + TRADING + The instrument is trading. + HALT + Trading in the instrument has been halted. + PAUSE + Trading in the instrument has been paused. + SUSPEND + Trading in the instrument has been suspended. + PRE_CLOSE + The instrument is in a pre-close period. + CLOSE + Trading in the instrument has closed. + POST_CLOSE + The instrument is in a post-close period. + SSR_CHANGE + A change in short-selling restrictions. + NOT_AVAILABLE_FOR_TRADING + The instrument is not available for trading, either trading has closed or been halted. + + """ + + NONE: int + PRE_OPEN: int + PRE_CROSS: int + QUOTING: int + CROSS: int + ROTATION: int + NEW_PRICE_INDICATION: int + TRADING: int + HALT: int + PAUSE: int + SUSPEND: int + PRE_CLOSE: int + CLOSE: int + POST_CLOSE: int + SSR_CHANGE: int + NOT_AVAILABLE_FOR_TRADING: int + + @classmethod + def variants(cls) -> Iterable[StatusAction]: ... + +class StatusReason(Enum): + """ + The secondary enum for a `StatusMsg` update, explains the cause of a halt or other change in + `action`. + + NONE + No reason is given. + SCHEDULED + The change in status occurred as scheduled. + SURVEILLANCE_INTERVENTION + The instrument stopped due to a market surveillance intervention. + MARKET_EVENT + The status changed due to activity in the market. + INSTRUMENT_ACTIVATION + The derivative instrument began trading. + INSTRUMENT_EXPIRATION + The derivative instrument expired. + RECOVERY_IN_PROCESS + Recovery in progress. + REGULATORY + The status change was caused by a regulatory action. + ADMINISTRATIVE + The status change was caused by an administrative action. + NON_COMPLIANCE + The status change was caused by the issuer not being compliance with regulatory + requirements. + FILINGS_NOT_CURRENT + Trading halted because the issuer's filings are not current. + SEC_TRADING_SUSPENSION + Trading halted due to an SEC trading suspension. + NEW_ISSUE + The status changed because a new issue is available. + ISSUE_AVAILABLE + The status changed because an issue is available. + ISSUES_REVIEWED + The status changed because the issue(s) were reviewed. + FILING_REQS_SATISFIED + The status changed because the filing requirements were satisfied. + NEWS_PENDING + Relevant news is pending. + NEWS_RELEASED + Relevant news was released. + NEWS_AND_RESUMPTION_TIMES + The news has been fully disseminated and times are available for the resumption in quoting + and trading. + NEWS_NOT_FORTHCOMING + The relevants news was not forthcoming. + ORDER_IMBALANCE + Halted for order imbalance. + LULD_PAUSE + The instrument hit limit up or limit down. + OPERATIONAL + An operational issue occurred with the venue. + ADDITIONAL_INFORMATION_REQUESTED + The status changed until the exchange receives additional information. + MERGER_EFFECTIVE + Trading halted due to merger becoming effective. + ETF + Trading is halted in an ETF due to conditions with the component securities. + CORPORATE_ACTION + Trading is halted for a corporate action. + NEW_SECURITY_OFFERING + Trading is halted because the instrument is a new offering. + MARKET_WIDE_HALT_LEVEL1 + Halted due to the market-wide circuit breaker level 1. + MARKET_WIDE_HALT_LEVEL2 + Halted due to the market-wide circuit breaker level 2. + MARKET_WIDE_HALT_LEVEL3 + Halted due to the market-wide circuit breaker level 3. + MARKET_WIDE_HALT_CARRYOVER + Halted due to the carryover of a market-wide circuit breaker from the previous trading day. + MARKET_WIDE_HALT_RESUMPTION + Resumption due to the end of the a market-wide circuit breaker halt. + QUOTATION_NOT_AVAILABLE + Halted because quotation is not available. + + """ + + NONE: int + SCHEDULED: int + SURVEILLANCE_INTERVENTION: int + MARKET_EVENT: int + INSTRUMENT_ACTIVATION: int + INSTRUMENT_EXPIRATION: int + RECOVERY_IN_PROCESS: int + REGULATORY: int + ADMINISTRATIVE: int + NON_COMPLIANCE: int + FILINGS_NOT_CURRENT: int + SEC_TRADING_SUSPENSION: int + NEW_ISSUE: int + ISSUE_AVAILABLE: int + ISSUES_REVIEWED: int + FILING_REQS_SATISFIED: int + NEWS_PENDING: int + NEWS_RELEASED: int + NEWS_AND_RESUMPTION_TIMES: int + NEWS_NOT_FORTHCOMING: int + ORDER_IMBALANCE: int + LULD_PAUSE: int + OPERATIONAL: int + ADDITIONAL_INFORMATION_REQUESTED: int + MERGER_EFFECTIVE: int + ETF: int + CORPORATE_ACTION: int + NEW_SECURITY_OFFERING: int + MARKET_WIDE_HALT_CARRYOVER: int + MARKET_WIDE_HALT_RESUMPTION: int + QUOTATION_NOT_AVAILABLE: int + + @classmethod + def variants(cls) -> Iterable[StatusReason]: ... + +class TradingEvent(Enum): + """ + Further information about a status update. + + + NONE + No additional information given. + NO_CANCEL + Order entry and modification are not allowed. + CHANGE_TRADING_SESSION + A change of trading session occurred. Daily statistics are reset. + IMPLIED_MATCHING_ON + Implied matching is available. + IMPLIED_MATCHING_OFF + Implied matching is not available. + + """ + + NONE: int + NO_CANCEL: int + CHANGE_TRADING_SESSION: int + IMPLIED_MATCHING_ON: int + IMPLIED_MATCHING_OFF: int + + @classmethod + def variants(cls) -> Iterable[TradingEvent]: ... + +class TriState(Enum): + """ + An enum for representing unknown, true, or false values. Equivalent to `Optional[bool]`. + + NOT_AVAILABLE + The value is not applicable or not known. + NO + False + YES + True + + """ + + NOT_AVAILABLE: str + NO: str + YES: str + + @classmethod + def from_str(cls, value: str) -> TriState: ... + @classmethod + def variants(cls) -> Iterable[TriState]: ... + def opt_bool(self) -> Optional[bool]: ... + +class VersionUpgradePolicy(Enum): + """ + How to handle decoding a DBN data from a prior version. + + AS_IS + Decode data from previous versions as-is. + UPGRADE + Decode data from previous versions converting it to the latest version. + + """ + + AS_IS: int + UPGRADE: int class Metadata(SupportsBytes): """ @@ -284,6 +792,7 @@ class Metadata(SupportsBytes): int """ + @property def dataset(self) -> str: """ @@ -294,6 +803,7 @@ class Metadata(SupportsBytes): str """ + @property def schema(self) -> str | None: """ @@ -305,6 +815,7 @@ class Metadata(SupportsBytes): str | None """ + @property def start(self) -> int: """ @@ -316,6 +827,7 @@ class Metadata(SupportsBytes): int """ + @property def end(self) -> int: """ @@ -327,6 +839,7 @@ class Metadata(SupportsBytes): int """ + @property def limit(self) -> int: """ @@ -337,6 +850,7 @@ class Metadata(SupportsBytes): int """ + @property def stype_in(self) -> SType | None: """ @@ -347,6 +861,7 @@ class Metadata(SupportsBytes): SType | None """ + @property def stype_out(self) -> SType: """ @@ -357,6 +872,7 @@ class Metadata(SupportsBytes): SType """ + @property def ts_out(self) -> bool: """ @@ -368,6 +884,7 @@ class Metadata(SupportsBytes): bool """ + @property def symbols(self) -> list[str]: """ @@ -378,6 +895,7 @@ class Metadata(SupportsBytes): list[str] """ + @property def partial(self) -> list[str]: """ @@ -389,6 +907,7 @@ class Metadata(SupportsBytes): list[str] """ + @property def not_found(self) -> list[str]: """ @@ -399,6 +918,7 @@ class Metadata(SupportsBytes): list[str] """ + @property def mappings(self) -> dict[str, list[MappingIntervalDict]]: """ @@ -409,10 +929,9 @@ class Metadata(SupportsBytes): dict[str, list[dict[str, Any]]]: """ + @classmethod - def decode( - cls, data: bytes, upgrade_policy: VersionUpgradePolicy | None = None - ) -> Metadata: + def decode(cls, data: bytes, upgrade_policy: VersionUpgradePolicy | None = None) -> Metadata: """ Decode the given Python `bytes` to `Metadata`. Returns a `Metadata` object with all the DBN metadata attributes. @@ -434,6 +953,7 @@ class Metadata(SupportsBytes): When a Metadata instance cannot be parsed from `data`. """ + def encode(self) -> bytes: """ Encode the Metadata to bytes. @@ -464,6 +984,7 @@ class RecordHeader: int """ + @property def rtype(self) -> int: """ @@ -474,6 +995,7 @@ class RecordHeader: int """ + @property def publisher_id(self) -> int: """ @@ -484,6 +1006,7 @@ class RecordHeader: int """ + @property def instrument_id(self) -> int: """ @@ -494,6 +1017,7 @@ class RecordHeader: int """ + @property def ts_event(self) -> int: """ @@ -531,6 +1055,7 @@ class Record(SupportsBytes): RecordHeader """ + @property def record_size(self) -> int: """ @@ -545,6 +1070,7 @@ class Record(SupportsBytes): size_hint """ + @property def rtype(self) -> int: """ @@ -555,6 +1081,7 @@ class Record(SupportsBytes): int """ + @property def publisher_id(self) -> int: """ @@ -565,6 +1092,7 @@ class Record(SupportsBytes): int """ + @property def instrument_id(self) -> int: """ @@ -575,6 +1103,7 @@ class Record(SupportsBytes): int """ + @property def pretty_ts_event(self) -> dt.datetime: """ @@ -586,6 +1115,7 @@ class Record(SupportsBytes): datetime.datetime """ + @property def ts_event(self) -> int: """ @@ -597,6 +1127,7 @@ class Record(SupportsBytes): int """ + @property def ts_out(self) -> int | None: """ @@ -624,6 +1155,7 @@ class _MBOBase: int """ + @property def pretty_price(self) -> float: """ @@ -638,6 +1170,7 @@ class _MBOBase: price """ + @property def price(self) -> int: """ @@ -653,6 +1186,7 @@ class _MBOBase: pretty_price """ + @property def size(self) -> int: """ @@ -663,6 +1197,7 @@ class _MBOBase: int """ + @property def flags(self) -> int: """ @@ -673,6 +1208,7 @@ class _MBOBase: int """ + @property def channel_id(self) -> int: """ @@ -683,6 +1219,7 @@ class _MBOBase: int """ + @property def action(self) -> str: """ @@ -694,6 +1231,7 @@ class _MBOBase: str """ + @property def side(self) -> str: """ @@ -706,6 +1244,7 @@ class _MBOBase: str """ + @property def pretty_ts_recv(self) -> dt.datetime: """ @@ -717,6 +1256,7 @@ class _MBOBase: datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -728,6 +1268,7 @@ class _MBOBase: int """ + @property def ts_in_delta(self) -> int: """ @@ -738,6 +1279,7 @@ class _MBOBase: int """ + @property def sequence(self) -> int: """ @@ -773,6 +1315,7 @@ class BidAskPair: bid_px """ + @property def bid_px(self) -> int: """ @@ -788,6 +1331,7 @@ class BidAskPair: pretty_bid_px """ + @property def pretty_ask_px(self) -> float: """ @@ -802,6 +1346,7 @@ class BidAskPair: ask_px """ + @property def ask_px(self) -> int: """ @@ -817,6 +1362,7 @@ class BidAskPair: pretty_ask_px """ + @property def bid_sz(self) -> int: """ @@ -827,6 +1373,7 @@ class BidAskPair: int """ + @property def ask_sz(self) -> int: """ @@ -837,30 +1384,333 @@ class BidAskPair: int """ + @property def bid_ct(self) -> int: """ The bid order count. - Returns - ------- - int + Returns + ------- + int + + """ + + @property + def ask_ct(self) -> int: + """ + The ask order count. + + Returns + ------- + int + + """ + +class ConsolidatedBidAskPair: + """ + A consolidated book level. + """ + + @property + def pretty_bid_px(self) -> float: + """ + The bid price as a float. + + Returns + ------- + float + + See Also + -------- + bid_px + + """ + + @property + def bid_px(self) -> int: + """ + The bid price expressed as a signed integer where every 1 unit + corresponds to 1e-9, i.e. 1/1,000,000,000 or 0.000000001. + + Returns + ------- + int + + See Also + -------- + pretty_bid_px + + """ + + @property + def pretty_ask_px(self) -> float: + """ + The ask price as a float. + + Returns + ------- + float + + See Also + -------- + ask_px + + """ + + @property + def ask_px(self) -> int: + """ + The ask price as a signed integer where every 1 unit + corresponds to 1e-9, i.e. 1/1,000,000,000 or 0.000000001. + + Returns + ------- + int + + See Also + -------- + pretty_ask_px + + """ + + @property + def bid_sz(self) -> int: + """ + The bid size. + + Returns + ------- + int + + """ + + @property + def ask_sz(self) -> int: + """ + The ask size. + + Returns + ------- + int + + """ + + @property + def bid_pb(self) -> int: + """ + The bid publisher. + + Returns + ------- + int + + """ + + @property + def pretty_bid_pb(self) -> Optional[str]: + """ + The human-readable bid publisher. + + Returns + ------- + Optional[str] + + """ + + @property + def ask_pb(self) -> int: + """ + The ask publisher. + + Returns + ------- + int + + """ + + @property + def pretty_ask_pb(self) -> Optional[str]: + """ + The human-readable ask publisher. + + Returns + ------- + Optional[str] + + """ + +class _MBPBase: + """ + Base for market-by-price messages. + """ + + @property + def pretty_price(self) -> float: + """ + The order price as a float. + + Returns + ------- + float + + See Also + -------- + price + + """ + + @property + def price(self) -> int: + """ + The order price expressed as a signed integer where every 1 unit + corresponds to 1e-9, i.e. 1/1,000,000,000 or 0.000000001. + + Returns + ------- + int + + See Also + -------- + pretty_price + + """ + + @property + def size(self) -> int: + """ + The order quantity. + + Returns + ------- + int + + """ + + @property + def action(self) -> str: + """ + The event action. Can be `A`dd, `C`ancel, `M`odify, clea`R`, or + `T`rade. + + Returns + ------- + str + + """ + + @property + def side(self) -> str: + """ + The side that initiates the event. Can be `A`sk for a sell order (or sell + aggressor in a trade), `B`id for a buy order (or buy aggressor in a trade), or + `N`one where no side is specified by the original source. + + Returns + ------- + str + + """ + + @property + def flags(self) -> int: + """ + A combination of packet end with matching engine status. + + Returns + ------- + int + + """ + + @property + def depth(self) -> int: + """ + The depth of actual book change. + + Returns + ------- + int + + """ + + @property + def pretty_ts_recv(self) -> dt.datetime: + """ + The capture-server-received timestamp as a datetime or + `pandas.Timestamp`, if available. + + Returns + ------- + datetime.datetime + + """ + + @property + def ts_recv(self) -> int: + """ + The capture-server-received timestamp expressed as number of + nanoseconds since the UNIX epoch. + + Returns + ------- + int + + """ + + @property + def ts_in_delta(self) -> int: + """ + The delta of `ts_recv - ts_exchange_send`, max 2 seconds. + + Returns + ------- + int + + """ + + @property + def sequence(self) -> int: + """ + The message sequence number assigned at the venue. + + Returns + ------- + int + + """ + +class TradeMsg(Record, _MBPBase): + """ + Market by price implementation with a book depth of 0. + + Equivalent to MBP-0. The record of the `Trades` schema. + + """ + +class MBP1Msg(Record, _MBPBase): + """ + Market by price implementation with a known book depth of 1. + """ - """ @property - def ask_ct(self) -> int: + def levels(self) -> list[BidAskPair]: """ - The ask order count. + The top of the order book. Returns ------- - int + list[BidAskPair] + + Notes + ----- + MBP1Msg contains 1 level of BidAskPair. """ -class _MBPBase: +class CbboMsg(Record): """ - Base for market-by-price messages. + Consolidated best bid and offer implementation. """ @property @@ -877,6 +1727,7 @@ class _MBPBase: price """ + @property def price(self) -> int: """ @@ -892,6 +1743,7 @@ class _MBPBase: pretty_price """ + @property def size(self) -> int: """ @@ -902,6 +1754,7 @@ class _MBPBase: int """ + @property def action(self) -> str: """ @@ -913,18 +1766,18 @@ class _MBPBase: str """ + @property def side(self) -> str: """ - The side that initiates the event. Can be `A`sk for a sell order (or sell - aggressor in a trade), `B`id for a buy order (or buy aggressor in a trade), or - `N`one where no side is specified by the original source. + The order side. Can be `A`sk, `B`id or `N`one. Returns ------- str """ + @property def flags(self) -> int: """ @@ -935,6 +1788,7 @@ class _MBPBase: int """ + @property def depth(self) -> int: """ @@ -945,6 +1799,7 @@ class _MBPBase: int """ + @property def pretty_ts_recv(self) -> dt.datetime: """ @@ -956,6 +1811,7 @@ class _MBPBase: datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -967,6 +1823,7 @@ class _MBPBase: int """ + @property def ts_in_delta(self) -> int: """ @@ -977,6 +1834,7 @@ class _MBPBase: int """ + @property def sequence(self) -> int: """ @@ -988,31 +1846,18 @@ class _MBPBase: """ -class TradeMsg(Record, _MBPBase): - """ - Market by price implementation with a book depth of 0. - - Equivalent to MBP-0. The record of the `Trades` schema. - - """ - -class MBP1Msg(Record, _MBPBase): - """ - Market by price implementation with a known book depth of 1. - """ - @property - def levels(self) -> list[BidAskPair]: + def levels(self) -> list[ConsolidatedBidAskPair]: """ - The top of the order book. + The top of the consolidated order book. Returns ------- - list[BidAskPair] + list[ConsolidatedBidAskPair] Notes ----- - MBP1Msg contains 1 level of BidAskPair. + CbboMsg contains 1 level of ConsolidatedBidAskPair. """ @@ -1055,6 +1900,7 @@ class OHLCVMsg(Record): open """ + @property def open(self) -> int: """ @@ -1070,6 +1916,7 @@ class OHLCVMsg(Record): pretty_open """ + @property def pretty_high(self) -> float: """ @@ -1084,6 +1931,7 @@ class OHLCVMsg(Record): high """ + @property def high(self) -> int: """ @@ -1099,6 +1947,7 @@ class OHLCVMsg(Record): pretty_high """ + @property def pretty_low(self) -> float: """ @@ -1113,6 +1962,7 @@ class OHLCVMsg(Record): low """ + @property def low(self) -> int: """ @@ -1128,6 +1978,7 @@ class OHLCVMsg(Record): pretty_low """ + @property def pretty_close(self) -> float: """ @@ -1142,6 +1993,7 @@ class OHLCVMsg(Record): close """ + @property def close(self) -> int: """ @@ -1157,6 +2009,7 @@ class OHLCVMsg(Record): pretty_close """ + @property def volume(self) -> int: """ @@ -1184,6 +2037,7 @@ class InstrumentDefMsg(Record): datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -1195,6 +2049,7 @@ class InstrumentDefMsg(Record): int """ + @property def pretty_min_price_increment(self) -> float: """ @@ -1209,6 +2064,7 @@ class InstrumentDefMsg(Record): min_price_increment """ + @property def min_price_increment(self) -> int: """ @@ -1224,6 +2080,7 @@ class InstrumentDefMsg(Record): pretty_min_price_increment """ + @property def display_factor(self) -> int: """ @@ -1235,6 +2092,7 @@ class InstrumentDefMsg(Record): int """ + @property def pretty_expiration(self) -> dt.datetime: """ @@ -1246,6 +2104,7 @@ class InstrumentDefMsg(Record): datetime.datetime """ + @property def expiration(self) -> int: """ @@ -1257,6 +2116,7 @@ class InstrumentDefMsg(Record): int """ + @property def pretty_activation(self) -> dt.datetime: """ @@ -1268,6 +2128,7 @@ class InstrumentDefMsg(Record): datetime.datetime """ + @property def activation(self) -> int: """ @@ -1279,6 +2140,7 @@ class InstrumentDefMsg(Record): int """ + @property def pretty_high_limit_price(self) -> float: """ @@ -1293,6 +2155,7 @@ class InstrumentDefMsg(Record): high_limit_price """ + @property def high_limit_price(self) -> int: """ @@ -1308,6 +2171,7 @@ class InstrumentDefMsg(Record): pretty_high_limit_price """ + @property def pretty_low_limit_price(self) -> float: """ @@ -1322,6 +2186,7 @@ class InstrumentDefMsg(Record): low_limit_price """ + @property def low_limit_price(self) -> int: """ @@ -1337,6 +2202,7 @@ class InstrumentDefMsg(Record): pretty_low_limit_price """ + @property def pretty_max_price_variation(self) -> float: """ @@ -1351,6 +2217,7 @@ class InstrumentDefMsg(Record): max_price_variation """ + @property def max_price_variation(self) -> int: """ @@ -1366,6 +2233,7 @@ class InstrumentDefMsg(Record): pretty_max_price_variation """ + @property def pretty_trading_reference_price(self) -> float: """ @@ -1380,6 +2248,7 @@ class InstrumentDefMsg(Record): trading_reference_price """ + @property def trading_reference_price(self) -> int: """ @@ -1395,6 +2264,7 @@ class InstrumentDefMsg(Record): pretty_trading_reference_price """ + @property def unit_of_measure_qty(self) -> int: """ @@ -1406,6 +2276,7 @@ class InstrumentDefMsg(Record): int """ + @property def pretty_min_price_increment_amount(self) -> float: """ @@ -1420,6 +2291,7 @@ class InstrumentDefMsg(Record): min_price_increment_amount """ + @property def min_price_increment_amount(self) -> int: """ @@ -1435,6 +2307,7 @@ class InstrumentDefMsg(Record): pretty_min_price_increment_amount """ + @property def pretty_price_ratio(self) -> float: """ @@ -1450,6 +2323,7 @@ class InstrumentDefMsg(Record): price_ratio """ + @property def price_ratio(self) -> int: """ @@ -1465,6 +2339,7 @@ class InstrumentDefMsg(Record): pretty_price_ratio """ + @property def inst_attrib_value(self) -> int: """ @@ -1475,6 +2350,7 @@ class InstrumentDefMsg(Record): int """ + @property def underlying_id(self) -> int: """ @@ -1485,6 +2361,7 @@ class InstrumentDefMsg(Record): int """ + @property def raw_instrument_id(self) -> int: """ @@ -1495,6 +2372,7 @@ class InstrumentDefMsg(Record): int """ + @property def market_depth_implied(self) -> int: """ @@ -1505,6 +2383,7 @@ class InstrumentDefMsg(Record): int """ + @property def market_depth(self) -> int: """ @@ -1515,6 +2394,7 @@ class InstrumentDefMsg(Record): int """ + @property def market_segment_id(self) -> int: """ @@ -1525,6 +2405,7 @@ class InstrumentDefMsg(Record): int """ + @property def max_trade_vol(self) -> int: """ @@ -1535,6 +2416,7 @@ class InstrumentDefMsg(Record): int """ + @property def min_lot_size(self) -> int: """ @@ -1545,6 +2427,7 @@ class InstrumentDefMsg(Record): int """ + @property def min_lot_size_block(self) -> int: """ @@ -1555,6 +2438,7 @@ class InstrumentDefMsg(Record): int """ + @property def min_lot_size_round_lot(self) -> int: """ @@ -1566,6 +2450,7 @@ class InstrumentDefMsg(Record): int """ + @property def min_trade_vol(self) -> int: """ @@ -1576,6 +2461,7 @@ class InstrumentDefMsg(Record): int """ + @property def contract_multiplier(self) -> int: """ @@ -1586,6 +2472,7 @@ class InstrumentDefMsg(Record): int """ + @property def decay_quantity(self) -> int: """ @@ -1597,6 +2484,7 @@ class InstrumentDefMsg(Record): int """ + @property def original_contract_size(self) -> int: """ @@ -1607,6 +2495,7 @@ class InstrumentDefMsg(Record): int """ + @property def trading_reference_date(self) -> int: """ @@ -1618,6 +2507,7 @@ class InstrumentDefMsg(Record): int """ + @property def appl_id(self) -> int: """ @@ -1628,6 +2518,7 @@ class InstrumentDefMsg(Record): int """ + @property def maturity_year(self) -> int: """ @@ -1638,6 +2529,7 @@ class InstrumentDefMsg(Record): int """ + @property def decay_start_date(self) -> int: """ @@ -1648,6 +2540,7 @@ class InstrumentDefMsg(Record): int """ + @property def channel_id(self) -> int: """ @@ -1659,6 +2552,7 @@ class InstrumentDefMsg(Record): int """ + @property def currency(self) -> str: """ @@ -1669,6 +2563,7 @@ class InstrumentDefMsg(Record): str """ + @property def settl_currency(self) -> str: """ @@ -1679,6 +2574,7 @@ class InstrumentDefMsg(Record): str """ + @property def secsubtype(self) -> str: """ @@ -1689,6 +2585,7 @@ class InstrumentDefMsg(Record): str """ + @property def raw_symbol(self) -> str: """ @@ -1699,6 +2596,7 @@ class InstrumentDefMsg(Record): str """ + @property def group(self) -> str: """ @@ -1709,6 +2607,7 @@ class InstrumentDefMsg(Record): str """ + @property def exchange(self) -> str: """ @@ -1719,6 +2618,7 @@ class InstrumentDefMsg(Record): str """ + @property def asset(self) -> str: """ @@ -1729,6 +2629,7 @@ class InstrumentDefMsg(Record): str """ + @property def cfi(self) -> str: """ @@ -1739,6 +2640,7 @@ class InstrumentDefMsg(Record): str """ + @property def security_type(self) -> str: """ @@ -1749,6 +2651,7 @@ class InstrumentDefMsg(Record): str """ + @property def unit_of_measure(self) -> str: """ @@ -1760,6 +2663,7 @@ class InstrumentDefMsg(Record): str """ + @property def underlying(self) -> str: """ @@ -1770,6 +2674,7 @@ class InstrumentDefMsg(Record): str """ + @property def strike_price_currency(self) -> str: """ @@ -1780,6 +2685,7 @@ class InstrumentDefMsg(Record): str """ + @property def instrument_class(self) -> str: """ @@ -1790,6 +2696,7 @@ class InstrumentDefMsg(Record): str """ + @property def pretty_strike_price(self) -> float: """ @@ -1804,6 +2711,7 @@ class InstrumentDefMsg(Record): strike_price """ + @property def strike_price(self) -> int: """ @@ -1819,6 +2727,7 @@ class InstrumentDefMsg(Record): pretty_strike_price """ + @property def match_algorithm(self) -> str: """ @@ -1829,6 +2738,7 @@ class InstrumentDefMsg(Record): str """ + @property def md_security_trading_status(self) -> int: """ @@ -1839,6 +2749,7 @@ class InstrumentDefMsg(Record): int """ + @property def main_fraction(self) -> int: """ @@ -1849,6 +2760,7 @@ class InstrumentDefMsg(Record): int """ + @property def price_display_format(self) -> int: """ @@ -1860,6 +2772,7 @@ class InstrumentDefMsg(Record): int """ + @property def settl_price_type(self) -> int: """ @@ -1870,6 +2783,7 @@ class InstrumentDefMsg(Record): int """ + @property def sub_fraction(self) -> int: """ @@ -1880,6 +2794,7 @@ class InstrumentDefMsg(Record): int """ + @property def underlying_product(self) -> int: """ @@ -1890,6 +2805,7 @@ class InstrumentDefMsg(Record): int """ + @property def security_update_action(self) -> str: """ @@ -1901,6 +2817,7 @@ class InstrumentDefMsg(Record): str """ + @property def maturity_month(self) -> int: """ @@ -1911,6 +2828,7 @@ class InstrumentDefMsg(Record): int """ + @property def maturity_day(self) -> int: """ @@ -1921,6 +2839,7 @@ class InstrumentDefMsg(Record): int """ + @property def maturity_week(self) -> int: """ @@ -1931,6 +2850,7 @@ class InstrumentDefMsg(Record): int """ + @property def user_defined_instrument(self) -> str: """ @@ -1941,6 +2861,7 @@ class InstrumentDefMsg(Record): str """ + @property def contract_multiplier_unit(self) -> int: """ @@ -1952,6 +2873,7 @@ class InstrumentDefMsg(Record): int """ + @property def flow_schedule_type(self) -> int: """ @@ -1962,6 +2884,7 @@ class InstrumentDefMsg(Record): int """ + @property def tick_rule(self) -> int: """ @@ -1989,6 +2912,7 @@ class InstrumentDefMsgV1(Record): datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -2000,6 +2924,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def pretty_min_price_increment(self) -> float: """ @@ -2014,6 +2939,7 @@ class InstrumentDefMsgV1(Record): min_price_increment """ + @property def min_price_increment(self) -> int: """ @@ -2029,6 +2955,7 @@ class InstrumentDefMsgV1(Record): pretty_min_price_increment """ + @property def display_factor(self) -> int: """ @@ -2040,6 +2967,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def pretty_expiration(self) -> dt.datetime: """ @@ -2051,6 +2979,7 @@ class InstrumentDefMsgV1(Record): datetime.datetime """ + @property def expiration(self) -> int: """ @@ -2062,6 +2991,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def pretty_activation(self) -> dt.datetime: """ @@ -2073,6 +3003,7 @@ class InstrumentDefMsgV1(Record): datetime.datetime """ + @property def activation(self) -> int: """ @@ -2084,6 +3015,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def pretty_high_limit_price(self) -> float: """ @@ -2098,6 +3030,7 @@ class InstrumentDefMsgV1(Record): high_limit_price """ + @property def high_limit_price(self) -> int: """ @@ -2113,6 +3046,7 @@ class InstrumentDefMsgV1(Record): pretty_high_limit_price """ + @property def pretty_low_limit_price(self) -> float: """ @@ -2127,6 +3061,7 @@ class InstrumentDefMsgV1(Record): low_limit_price """ + @property def low_limit_price(self) -> int: """ @@ -2142,6 +3077,7 @@ class InstrumentDefMsgV1(Record): pretty_low_limit_price """ + @property def pretty_max_price_variation(self) -> float: """ @@ -2156,6 +3092,7 @@ class InstrumentDefMsgV1(Record): max_price_variation """ + @property def max_price_variation(self) -> int: """ @@ -2171,6 +3108,7 @@ class InstrumentDefMsgV1(Record): pretty_max_price_variation """ + @property def pretty_trading_reference_price(self) -> float: """ @@ -2185,6 +3123,7 @@ class InstrumentDefMsgV1(Record): trading_reference_price """ + @property def trading_reference_price(self) -> int: """ @@ -2200,6 +3139,7 @@ class InstrumentDefMsgV1(Record): pretty_trading_reference_price """ + @property def unit_of_measure_qty(self) -> int: """ @@ -2211,6 +3151,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def pretty_min_price_increment_amount(self) -> float: """ @@ -2225,6 +3166,7 @@ class InstrumentDefMsgV1(Record): min_price_increment_amount """ + @property def min_price_increment_amount(self) -> int: """ @@ -2240,6 +3182,7 @@ class InstrumentDefMsgV1(Record): pretty_min_price_increment_amount """ + @property def pretty_price_ratio(self) -> float: """ @@ -2255,6 +3198,7 @@ class InstrumentDefMsgV1(Record): price_ratio """ + @property def price_ratio(self) -> int: """ @@ -2270,6 +3214,7 @@ class InstrumentDefMsgV1(Record): pretty_price_ratio """ + @property def inst_attrib_value(self) -> int: """ @@ -2280,6 +3225,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def underlying_id(self) -> int: """ @@ -2290,6 +3236,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def raw_instrument_id(self) -> int: """ @@ -2300,6 +3247,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def market_depth_implied(self) -> int: """ @@ -2310,6 +3258,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def market_depth(self) -> int: """ @@ -2320,6 +3269,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def market_segment_id(self) -> int: """ @@ -2330,6 +3280,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def max_trade_vol(self) -> int: """ @@ -2340,6 +3291,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def min_lot_size(self) -> int: """ @@ -2350,6 +3302,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def min_lot_size_block(self) -> int: """ @@ -2360,6 +3313,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def min_lot_size_round_lot(self) -> int: """ @@ -2371,6 +3325,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def min_trade_vol(self) -> int: """ @@ -2381,6 +3336,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def contract_multiplier(self) -> int: """ @@ -2391,6 +3347,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def decay_quantity(self) -> int: """ @@ -2402,6 +3359,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def original_contract_size(self) -> int: """ @@ -2412,6 +3370,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def trading_reference_date(self) -> int: """ @@ -2423,6 +3382,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def appl_id(self) -> int: """ @@ -2433,6 +3393,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def maturity_year(self) -> int: """ @@ -2443,6 +3404,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def decay_start_date(self) -> int: """ @@ -2453,6 +3415,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def channel_id(self) -> int: """ @@ -2464,6 +3427,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def currency(self) -> str: """ @@ -2474,6 +3438,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def settl_currency(self) -> str: """ @@ -2484,6 +3449,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def secsubtype(self) -> str: """ @@ -2494,6 +3460,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def raw_symbol(self) -> str: """ @@ -2504,6 +3471,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def group(self) -> str: """ @@ -2514,6 +3482,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def exchange(self) -> str: """ @@ -2524,6 +3493,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def asset(self) -> str: """ @@ -2534,6 +3504,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def cfi(self) -> str: """ @@ -2544,6 +3515,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def security_type(self) -> str: """ @@ -2554,6 +3526,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def unit_of_measure(self) -> str: """ @@ -2565,6 +3538,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def underlying(self) -> str: """ @@ -2575,6 +3549,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def strike_price_currency(self) -> str: """ @@ -2585,6 +3560,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def instrument_class(self) -> str: """ @@ -2595,6 +3571,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def pretty_strike_price(self) -> float: """ @@ -2609,6 +3586,7 @@ class InstrumentDefMsgV1(Record): strike_price """ + @property def strike_price(self) -> int: """ @@ -2624,6 +3602,7 @@ class InstrumentDefMsgV1(Record): pretty_strike_price """ + @property def match_algorithm(self) -> str: """ @@ -2634,6 +3613,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def md_security_trading_status(self) -> int: """ @@ -2644,6 +3624,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def main_fraction(self) -> int: """ @@ -2654,6 +3635,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def price_display_format(self) -> int: """ @@ -2665,6 +3647,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def settl_price_type(self) -> int: """ @@ -2675,6 +3658,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def sub_fraction(self) -> int: """ @@ -2685,6 +3669,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def underlying_product(self) -> int: """ @@ -2695,6 +3680,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def security_update_action(self) -> str: """ @@ -2706,6 +3692,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def maturity_month(self) -> int: """ @@ -2716,6 +3703,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def maturity_day(self) -> int: """ @@ -2726,6 +3714,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def maturity_week(self) -> int: """ @@ -2736,6 +3725,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def user_defined_instrument(self) -> str: """ @@ -2746,6 +3736,7 @@ class InstrumentDefMsgV1(Record): str """ + @property def contract_multiplier_unit(self) -> int: """ @@ -2757,6 +3748,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def flow_schedule_type(self) -> int: """ @@ -2767,6 +3759,7 @@ class InstrumentDefMsgV1(Record): int """ + @property def tick_rule(self) -> int: """ @@ -2794,6 +3787,7 @@ class ImbalanceMsg(Record): datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -2805,6 +3799,7 @@ class ImbalanceMsg(Record): int """ + @property def pretty_ref_price(self) -> float: """ @@ -2819,6 +3814,7 @@ class ImbalanceMsg(Record): ref_price """ + @property def ref_price(self) -> int: """ @@ -2834,6 +3830,7 @@ class ImbalanceMsg(Record): pretty_ref_price """ + @property def auction_time(self) -> int: """ @@ -2844,6 +3841,7 @@ class ImbalanceMsg(Record): int """ + @property def pretty_cont_book_clr_price(self) -> float: """ @@ -2859,6 +3857,7 @@ class ImbalanceMsg(Record): cont_book_clr_price """ + @property def cont_book_clr_price(self) -> int: """ @@ -2875,6 +3874,7 @@ class ImbalanceMsg(Record): pretty_cont_book_clr_price """ + @property def pretty_auct_interest_clr_price(self) -> float: """ @@ -2890,6 +3890,7 @@ class ImbalanceMsg(Record): auct_interest_clr_price """ + @property def auct_interest_clr_price(self) -> int: """ @@ -2905,6 +3906,7 @@ class ImbalanceMsg(Record): pretty_auct_interest_clr_price """ + @property def ssr_filling_price(self) -> int: """ @@ -2915,6 +3917,7 @@ class ImbalanceMsg(Record): int """ + @property def ind_match_price(self) -> int: """ @@ -2925,6 +3928,7 @@ class ImbalanceMsg(Record): int """ + @property def upper_collar(self) -> int: """ @@ -2935,6 +3939,7 @@ class ImbalanceMsg(Record): int """ + @property def lower_collar(self) -> int: """ @@ -2945,6 +3950,7 @@ class ImbalanceMsg(Record): int """ + @property def paired_qty(self) -> int: """ @@ -2955,6 +3961,7 @@ class ImbalanceMsg(Record): int """ + @property def total_imbalance_qty(self) -> int: """ @@ -2965,6 +3972,7 @@ class ImbalanceMsg(Record): int """ + @property def market_imbalance_qty(self) -> int: """ @@ -2975,6 +3983,7 @@ class ImbalanceMsg(Record): int """ + @property def unpaired_qty(self) -> int: """ @@ -2985,6 +3994,7 @@ class ImbalanceMsg(Record): int """ + @property def auction_type(self) -> str: """ @@ -2995,6 +4005,7 @@ class ImbalanceMsg(Record): str """ + @property def side(self) -> str: """ @@ -3006,6 +4017,7 @@ class ImbalanceMsg(Record): str """ + @property def auction_status(self) -> int: """ @@ -3016,6 +4028,7 @@ class ImbalanceMsg(Record): int """ + @property def freeze_status(self) -> int: """ @@ -3026,6 +4039,7 @@ class ImbalanceMsg(Record): int """ + @property def num_extensions(self) -> int: """ @@ -3036,6 +4050,7 @@ class ImbalanceMsg(Record): int """ + @property def unpaired_side(self) -> str: """ @@ -3046,6 +4061,7 @@ class ImbalanceMsg(Record): str """ + @property def significant_imbalance(self) -> str: """ @@ -3078,6 +4094,7 @@ class StatMsg(Record): datetime.datetime """ + @property def ts_recv(self) -> int: """ @@ -3089,6 +4106,7 @@ class StatMsg(Record): int """ + @property def ts_ref(self) -> int: """ @@ -3100,6 +4118,7 @@ class StatMsg(Record): int """ + @property def pretty_price(self) -> float: """ @@ -3114,6 +4133,7 @@ class StatMsg(Record): price """ + @property def price(self) -> int: """ @@ -3130,6 +4150,7 @@ class StatMsg(Record): pretty_price """ + @property def quantity(self) -> int: """ @@ -3140,6 +4161,7 @@ class StatMsg(Record): int """ + @property def sequence(self) -> int: """ @@ -3150,6 +4172,7 @@ class StatMsg(Record): int """ + @property def ts_in_delta(self) -> int: """ @@ -3160,6 +4183,7 @@ class StatMsg(Record): int """ + @property def stat_type(self) -> int: """ @@ -3170,6 +4194,7 @@ class StatMsg(Record): int """ + @property def channel_id(self) -> int: """ @@ -3180,6 +4205,7 @@ class StatMsg(Record): int """ + @property def update_action(self) -> int: """ @@ -3191,6 +4217,7 @@ class StatMsg(Record): int """ + @property def stat_flags(self) -> int: """ @@ -3217,6 +4244,7 @@ class ErrorMsg(Record): str """ + @property def is_last(self) -> int: """ @@ -3258,6 +4286,7 @@ class SymbolMappingMsg(Record): SType """ + @property def stype_in_symbol(self) -> str: """ @@ -3268,6 +4297,7 @@ class SymbolMappingMsg(Record): str """ + @property def stype_out(self) -> SType: """ @@ -3278,6 +4308,7 @@ class SymbolMappingMsg(Record): SType """ + @property def stype_out_symbol(self) -> str: """ @@ -3288,6 +4319,7 @@ class SymbolMappingMsg(Record): str """ + @property def pretty_start_ts(self) -> dt.datetime: """ @@ -3299,6 +4331,7 @@ class SymbolMappingMsg(Record): datetime.datetime """ + @property def start_ts(self) -> int: """ @@ -3310,6 +4343,7 @@ class SymbolMappingMsg(Record): int """ + @property def pretty_end_ts(self) -> dt.datetime: """ @@ -3321,6 +4355,7 @@ class SymbolMappingMsg(Record): datetime.datetime """ + @property def end_ts(self) -> int: """ @@ -3348,6 +4383,7 @@ class SymbolMappingMsgV1(Record): str """ + @property def stype_out_symbol(self) -> str: """ @@ -3358,6 +4394,7 @@ class SymbolMappingMsgV1(Record): str """ + @property def pretty_start_ts(self) -> dt.datetime: """ @@ -3369,6 +4406,7 @@ class SymbolMappingMsgV1(Record): datetime.datetime """ + @property def start_ts(self) -> int: """ @@ -3380,6 +4418,7 @@ class SymbolMappingMsgV1(Record): int """ + @property def pretty_end_ts(self) -> dt.datetime: """ @@ -3391,6 +4430,7 @@ class SymbolMappingMsgV1(Record): datetime.datetime """ + @property def end_ts(self) -> int: """ @@ -3421,6 +4461,7 @@ class SystemMsg(Record): str """ + @property def is_heartbeat(self) -> bool: """ @@ -3452,6 +4493,7 @@ class SystemMsgV1(Record): str """ + @property def is_heartbeat(self) -> bool: """ @@ -3499,6 +4541,7 @@ class DBNDecoder: bytes """ + def decode( self, ) -> list[_DBNRecord]: @@ -3519,6 +4562,7 @@ class DBNDecoder: write """ + def write( self, bytes: bytes, @@ -3589,8 +4633,7 @@ class Transcoder: map_symbols: bool = True, has_metadata: bool = True, ts_out: bool = False, - symbol_interval_map: dict[int, list[tuple[dt.date, dt.date, str]]] - | None = None, + symbol_interval_map: dict[int, list[tuple[dt.date, dt.date, str]]] | None = None, schema: Schema | None = None, input_version: int = 2, upgrade_policy: VersionUpgradePolicy | None = None, @@ -3603,6 +4646,7 @@ class Transcoder: ------- bytes """ + def write( self, bytes: bytes, @@ -3615,6 +4659,7 @@ class Transcoder: ValueError When the write to the internal buffer or the output fails. """ + def flush( self, ) -> None: diff --git a/python/src/encode.rs b/python/src/encode.rs index ab40034..309974b 100644 --- a/python/src/encode.rs +++ b/python/src/encode.rs @@ -11,8 +11,8 @@ use dbn::{ enums::{Compression, Schema}, python::to_val_err, record::{ - ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, OhlcvMsg, StatMsg, TbboMsg, - TradeMsg, + Bbo1MMsg, Bbo1SMsg, Cbbo1MMsg, Cbbo1SMsg, CbboMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, + Mbp10Msg, Mbp1Msg, OhlcvMsg, StatMsg, TbboMsg, TcbboMsg, TradeMsg, }, Metadata, }; @@ -88,6 +88,12 @@ pub fn write_dbn_file( Some(Schema::Status) | None => Err(PyValueError::new_err( "Unsupported schema type for writing DBN files", )), + Some(Schema::Cbbo) => encode_pyrecs::(encoder, &records), + Some(Schema::Cbbo1S) => encode_pyrecs::(encoder, &records), + Some(Schema::Cbbo1M) => encode_pyrecs::(encoder, &records), + Some(Schema::Tcbbo) => encode_pyrecs::(encoder, &records), + Some(Schema::Bbo1S) => encode_pyrecs::(encoder, &records), + Some(Schema::Bbo1M) => encode_pyrecs::(encoder, &records), } } diff --git a/python/src/lib.rs b/python/src/lib.rs index 30a1c49..3e95eaf 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -4,15 +4,14 @@ use pyo3::{prelude::*, wrap_pyfunction, PyClass}; use dbn::{ compat::{ErrorMsgV1, InstrumentDefMsgV1, SymbolMappingMsgV1, SystemMsgV1}, - enums::{Compression, Encoding, SType, Schema}, flags, python::EnumIterator, - record::{ - BidAskPair, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, OhlcvMsg, - RecordHeader, StatMsg, StatusMsg, SymbolMappingMsg, SystemMsg, TradeMsg, - }, - Metadata, RType, VersionUpgradePolicy, FIXED_PRICE_SCALE, UNDEF_ORDER_SIZE, UNDEF_PRICE, - UNDEF_STAT_QUANTITY, UNDEF_TIMESTAMP, + Action, BidAskPair, CbboMsg, Compression, ConsolidatedBidAskPair, Encoding, ErrorMsg, + ImbalanceMsg, InstrumentClass, InstrumentDefMsg, MatchAlgorithm, MboMsg, Mbp10Msg, Mbp1Msg, + Metadata, OhlcvMsg, RType, RecordHeader, SType, Schema, SecurityUpdateAction, Side, StatMsg, + StatType, StatUpdateAction, StatusAction, StatusMsg, StatusReason, SymbolMappingMsg, SystemMsg, + TradeMsg, TradingEvent, TriState, UserDefinedInstrument, VersionUpgradePolicy, DBN_VERSION, + FIXED_PRICE_SCALE, UNDEF_ORDER_SIZE, UNDEF_PRICE, UNDEF_STAT_QUANTITY, UNDEF_TIMESTAMP, }; mod dbn_decoder; @@ -39,6 +38,7 @@ fn databento_dbn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; + checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; @@ -54,14 +54,28 @@ fn databento_dbn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; + checked_add_class::(m)?; // PyClass enums + checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; + checked_add_class::(m)?; checked_add_class::(m)?; // constants + m.add("DBN_VERSION", DBN_VERSION)?; m.add("FIXED_PRICE_SCALE", FIXED_PRICE_SCALE)?; m.add("UNDEF_PRICE", UNDEF_PRICE)?; m.add("UNDEF_ORDER_SIZE", UNDEF_ORDER_SIZE)?; @@ -78,19 +92,23 @@ fn databento_dbn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { #[cfg(test)] mod tests { + use std::sync::Once; + use dbn::enums::SType; use super::*; pub const TEST_DATA_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data"); + pub static INIT: Once = Once::new(); + pub fn setup() { - if unsafe { pyo3::ffi::Py_IsInitialized() } == 0 { + INIT.call_once(|| { // add to available modules pyo3::append_to_inittab!(databento_dbn); - } - // initialize interpreter - pyo3::prepare_freethreaded_python(); + // initialize interpreter + pyo3::prepare_freethreaded_python(); + }); } #[test] diff --git a/rust/dbn-cli/Cargo.toml b/rust/dbn-cli/Cargo.toml index e99e8f5..7af0474 100644 --- a/rust/dbn-cli/Cargo.toml +++ b/rust/dbn-cli/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" [dependencies] # Databento common DBN library -dbn = { path = "../dbn", version = "=0.16.0", default-features = false } +dbn = { path = "../dbn", version = "=0.17.0", default-features = false } # Error handling anyhow = "1.0" diff --git a/rust/dbn/Cargo.toml b/rust/dbn/Cargo.toml index 66205a3..cd8090f 100644 --- a/rust/dbn/Cargo.toml +++ b/rust/dbn/Cargo.toml @@ -25,7 +25,7 @@ serde = ["dep:serde", "time/parsing", "time/serde"] trivial_copy = [] [dependencies] -dbn-macros = { version = "=0.16.0", path = "../dbn-macros" } +dbn-macros = { version = "=0.17.0", path = "../dbn-macros" } # async (de)compression async-compression = { version = "0.4.6", features = ["tokio", "zstd"], optional = true } diff --git a/rust/dbn/src/compat.rs b/rust/dbn/src/compat.rs index b9ac0bf..dccf964 100644 --- a/rust/dbn/src/compat.rs +++ b/rust/dbn/src/compat.rs @@ -642,7 +642,7 @@ mod tests { use time::OffsetDateTime; use type_layout::{Field, TypeLayout}; - use crate::{Mbp1Msg, Record, MAX_RECORD_LEN}; + use crate::{Mbp1Msg, Record, Schema, MAX_RECORD_LEN}; use super::*; @@ -725,7 +725,7 @@ mod tests { let rec = Mbp1Msg { price: 1_250_000_000, side: b'A' as c_char, - ..Default::default() + ..Mbp1Msg::default_for_schema(Schema::Mbp1) }; let orig = WithTsOut::new(rec, OffsetDateTime::now_utc().unix_timestamp_nanos() as u64); let mut compat_buffer = [0; MAX_RECORD_LEN]; diff --git a/rust/dbn/src/decode/dbn/async.rs b/rust/dbn/src/decode/dbn/async.rs index e5ac7e8..cab67ec 100644 --- a/rust/dbn/src/decode/dbn/async.rs +++ b/rust/dbn/src/decode/dbn/async.rs @@ -663,15 +663,16 @@ mod tests { dbn::{AsyncEncoder, AsyncRecordEncoder}, DbnEncodable, }, - rtype, Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, + rtype, CbboMsg, Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, OhlcvMsg, RecordHeader, Result, Schema, StatMsg, TbboMsg, TradeMsg, WithTsOut, }; #[rstest] #[case::mbo(Schema::Mbo, MboMsg::default())] #[case::trades(Schema::Trades, TradeMsg::default())] - #[case::tbbo(Schema::Tbbo, TbboMsg::default())] - #[case::mbp1(Schema::Mbp1, Mbp1Msg::default())] + #[case::tbbo(Schema::Cbbo, CbboMsg::default_for_schema(Schema::Cbbo))] + #[case::tbbo(Schema::Tbbo, TbboMsg::default_for_schema(Schema::Tbbo))] + #[case::mbp1(Schema::Mbp1, Mbp1Msg::default_for_schema(Schema::Mbp1))] #[case::mbp10(Schema::Mbp10, Mbp10Msg::default())] #[case::ohlcv1d(Schema::Ohlcv1D, OhlcvMsg::default_for_schema(Schema::Ohlcv1D))] #[case::ohlcv1h(Schema::Ohlcv1H, OhlcvMsg::default_for_schema(Schema::Ohlcv1H))] @@ -713,8 +714,9 @@ mod tests { #[rstest] #[case::mbo(Schema::Mbo, MboMsg::default())] #[case::trades(Schema::Trades, TradeMsg::default())] - #[case::tbbo(Schema::Tbbo, TbboMsg::default())] - #[case::mbp1(Schema::Mbp1, Mbp1Msg::default())] + #[case::cbbo(Schema::Cbbo, CbboMsg::default_for_schema(Schema::Cbbo))] + #[case::tbbo(Schema::Tbbo, TbboMsg::default_for_schema(Schema::Tbbo))] + #[case::mbp1(Schema::Mbp1, Mbp1Msg::default_for_schema(Schema::Mbp1))] #[case::mbp10(Schema::Mbp10, Mbp10Msg::default())] #[case::ohlcv1d(Schema::Ohlcv1D, OhlcvMsg::default_for_schema(Schema::Ohlcv1D))] #[case::ohlcv1h(Schema::Ohlcv1H, OhlcvMsg::default_for_schema(Schema::Ohlcv1H))] diff --git a/rust/dbn/src/decode/dbn/sync.rs b/rust/dbn/src/decode/dbn/sync.rs index 6919a0f..ada4b7e 100644 --- a/rust/dbn/src/decode/dbn/sync.rs +++ b/rust/dbn/src/decode/dbn/sync.rs @@ -674,9 +674,9 @@ mod tests { encode::{ dbn::Encoder, DbnEncodable, DbnRecordEncoder, DynWriter, EncodeDbn, EncodeRecord, }, - rtype, Compression, Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, - Mbp1Msg, MetadataBuilder, OhlcvMsg, RecordHeader, Result, StatMsg, TbboMsg, TradeMsg, - WithTsOut, SYMBOL_CSTR_LEN, + rtype, CbboMsg, Compression, Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, + Mbp10Msg, Mbp1Msg, MetadataBuilder, OhlcvMsg, RecordHeader, Result, StatMsg, TbboMsg, + TradeMsg, WithTsOut, SYMBOL_CSTR_LEN, }; #[test] @@ -726,8 +726,18 @@ mod tests { #[rstest] #[case::uncompressed_mbo_v1(1, Schema::Mbo, Compression::None, MboMsg::default())] #[case::uncompressed_trades_v1(1, Schema::Trades, Compression::None, TradeMsg::default())] - #[case::uncompressed_tbbo_v1(1, Schema::Tbbo, Compression::None, TbboMsg::default())] - #[case::uncompressed_mbp1_v1(1, Schema::Mbp1, Compression::None, Mbp1Msg::default())] + #[case::uncompressed_tbbo_v1( + 1, + Schema::Tbbo, + Compression::None, + TbboMsg::default_for_schema(Schema::Tbbo) + )] + #[case::uncompressed_mbp1_v1( + 1, + Schema::Mbp1, + Compression::None, + Mbp1Msg::default_for_schema(Schema::Tbbo) + )] #[case::uncompressed_mbp10_v1(1, Schema::Mbp10, Compression::None, Mbp10Msg::default())] #[case::uncompressed_ohlcv1d_v1( 1, @@ -773,8 +783,18 @@ mod tests { )] #[case::zstd_mbo_v1(1, Schema::Mbo, Compression::ZStd, MboMsg::default())] #[case::zstd_trades_v1(1, Schema::Trades, Compression::ZStd, TradeMsg::default())] - #[case::zstd_tbbo_v1(1, Schema::Tbbo, Compression::ZStd, TbboMsg::default())] - #[case::zstd_mbp1_v1(1, Schema::Mbp1, Compression::ZStd, Mbp1Msg::default())] + #[case::zstd_tbbo_v1( + 1, + Schema::Tbbo, + Compression::ZStd, + TbboMsg::default_for_schema(Schema::Tbbo) + )] + #[case::zstd_mbp1_v1( + 1, + Schema::Mbp1, + Compression::ZStd, + Mbp1Msg::default_for_schema(Schema::Mbp1) + )] #[case::zstd_mbp10_v1(1, Schema::Mbp10, Compression::ZStd, Mbp10Msg::default())] #[case::zstd_ohlcv1d_v1( 1, @@ -810,8 +830,18 @@ mod tests { #[case::zstd_statistics_v1(1, Schema::Statistics, Compression::ZStd, StatMsg::default())] #[case::uncompressed_mbo_v2(2, Schema::Mbo, Compression::None, MboMsg::default())] #[case::uncompressed_trades_v2(2, Schema::Trades, Compression::None, TradeMsg::default())] - #[case::uncompressed_tbbo_v2(2, Schema::Tbbo, Compression::None, TbboMsg::default())] - #[case::uncompressed_mbp1_v2(2, Schema::Mbp1, Compression::None, Mbp1Msg::default())] + #[case::uncompressed_tbbo_v2( + 2, + Schema::Tbbo, + Compression::None, + TbboMsg::default_for_schema(Schema::Tbbo) + )] + #[case::uncompressed_mbp1_v2( + 2, + Schema::Mbp1, + Compression::None, + Mbp1Msg::default_for_schema(Schema::Mbp1) + )] #[case::uncompressed_mbp10_v2(2, Schema::Mbp10, Compression::None, Mbp10Msg::default())] #[case::uncompressed_ohlcv1d_v2( 2, @@ -857,8 +887,24 @@ mod tests { )] #[case::zstd_mbo_v2(2, Schema::Mbo, Compression::ZStd, MboMsg::default())] #[case::zstd_trades_v2(2, Schema::Trades, Compression::ZStd, TradeMsg::default())] - #[case::zstd_tbbo_v2(2, Schema::Tbbo, Compression::ZStd, TbboMsg::default())] - #[case::zstd_mbp1_v2(2, Schema::Mbp1, Compression::ZStd, Mbp1Msg::default())] + #[case::zstd_tbbo_v2( + 2, + Schema::Tbbo, + Compression::ZStd, + TbboMsg::default_for_schema(Schema::Tbbo) + )] + #[case::zstd_mbp1_v2( + 2, + Schema::Mbp1, + Compression::ZStd, + Mbp1Msg::default_for_schema(Schema::Mbp1) + )] + #[case::zstd_cbbo_v2( + 2, + Schema::Cbbo, + Compression::ZStd, + CbboMsg::default_for_schema(Schema::Cbbo) + )] #[case::zstd_mbp10_v2(2, Schema::Mbp10, Compression::ZStd, Mbp10Msg::default())] #[case::zstd_ohlcv1d_v2( 2, diff --git a/rust/dbn/src/encode/csv/serialize.rs b/rust/dbn/src/encode/csv/serialize.rs index 7ec9e16..3a38298 100644 --- a/rust/dbn/src/encode/csv/serialize.rs +++ b/rust/dbn/src/encode/csv/serialize.rs @@ -5,7 +5,9 @@ use csv::Writer; use crate::{ enums::{SecurityUpdateAction, UserDefinedInstrument}, pretty::{fmt_px, fmt_ts}, - record::{c_chars_to_str, BidAskPair, HasRType, RecordHeader, WithTsOut}, + record::{ + c_chars_to_str, BidAskPair, ConsolidatedBidAskPair, HasRType, RecordHeader, WithTsOut, + }, UNDEF_PRICE, UNDEF_TIMESTAMP, }; @@ -88,6 +90,32 @@ impl WriteField for [BidAskPair; N] { Ok(()) } } + +impl WriteField for [ConsolidatedBidAskPair; N] { + fn write_header(csv_writer: &mut Writer, _name: &str) -> csv::Result<()> { + for i in 0..N { + for f in ["bid_px", "ask_px", "bid_sz", "ask_sz", "bid_pb", "ask_pb"] { + csv_writer.write_field(&format!("{f}_{i:02}"))?; + } + } + Ok(()) + } + + fn write_field( + &self, + writer: &mut csv::Writer, + ) -> csv::Result<()> { + for level in self.iter() { + write_px_field::(writer, level.bid_px)?; + write_px_field::(writer, level.ask_px)?; + level.bid_sz.write_field::(writer)?; + level.ask_sz.write_field::(writer)?; + level.bid_pb.write_field::(writer)?; + level.ask_pb.write_field::(writer)?; + } + Ok(()) + } +} macro_rules! impl_write_field_for { ($($ty:ident),+) => { $( diff --git a/rust/dbn/src/encode/json/serialize.rs b/rust/dbn/src/encode/json/serialize.rs index 5aaf80a..e875377 100644 --- a/rust/dbn/src/encode/json/serialize.rs +++ b/rust/dbn/src/encode/json/serialize.rs @@ -3,7 +3,7 @@ use std::ffi::c_char; use crate::{ json_writer::{JsonObjectWriter, NULL}, pretty::{fmt_px, fmt_ts}, - record::c_chars_to_str, + record::{c_chars_to_str, ConsolidatedBidAskPair}, BidAskPair, HasRType, Metadata, RecordHeader, SecurityUpdateAction, UserDefinedInstrument, WithTsOut, UNDEF_PRICE, UNDEF_TIMESTAMP, }; @@ -193,6 +193,29 @@ impl WriteField for [BidAskPair; N] { } } +impl WriteField for [ConsolidatedBidAskPair; N] { + fn write_field< + J: crate::json_writer::JsonWriter, + const PRETTY_PX: bool, + const PRETTY_TS: bool, + >( + &self, + writer: &mut JsonObjectWriter, + name: &str, + ) { + let mut arr_writer = writer.array(name); + for level in self.iter() { + let mut item_writer = arr_writer.object(); + write_px_field::(&mut item_writer, "bid_px", level.bid_px); + write_px_field::(&mut item_writer, "ask_px", level.ask_px); + item_writer.value("bid_sz", level.bid_sz); + item_writer.value("ask_sz", level.ask_sz); + item_writer.value("bid_pb", level.bid_pb); + item_writer.value("ask_pb", level.ask_pb); + } + } +} + impl WriteField for i64 { fn write_field< J: crate::json_writer::JsonWriter, diff --git a/rust/dbn/src/enums.rs b/rust/dbn/src/enums.rs index d6ccbfb..0e20ea1 100644 --- a/rust/dbn/src/enums.rs +++ b/rust/dbn/src/enums.rs @@ -15,6 +15,11 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; /// A side of the market. The side of the market for resting orders, or the side /// of the aggressor for trades. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter, strum::AsRefStr), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[repr(u8)] pub enum Side { /// A sell order or sell aggressor in a trade. @@ -33,6 +38,11 @@ impl From for char { /// A tick action. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter, strum::AsRefStr), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[repr(u8)] pub enum Action { /// An existing order was modified. @@ -57,6 +67,11 @@ impl From for char { /// The class of instrument. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[repr(u8)] pub enum InstrumentClass { /// A bond. @@ -86,10 +101,18 @@ impl From for char { } /// The type of matching algorithm used for the instrument at the exchange. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u8)] pub enum MatchAlgorithm { + /// No matching algorithm was specified. + #[default] + Undefined = b' ', /// First-in-first-out matching. Fifo = b'F', /// A configurable match algorithm. @@ -110,6 +133,9 @@ pub enum MatchAlgorithm { /// Special variant used only for Eurodollar futures on CME. See /// [CME documentation](https://www.cmegroup.com/confluence/display/EPICSANDBOX/Supported+Matching+Algorithms#SupportedMatchingAlgorithms-Pro-RataAllocationforEurodollarFutures). EurodollarFutures = b'Y', + /// Trade quantity is shared between all orders at the best price. Orders with the + /// highest time priority receive a higher matched quantity. + TimeProRata = b'P', } impl From for char { @@ -120,8 +146,13 @@ impl From for char { /// Whether the instrument is user-defined. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, IntoPrimitive, Default)] -#[repr(u8)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter, strum::AsRefStr), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(u8)] pub enum UserDefinedInstrument { /// The instrument is not user-defined. #[default] @@ -139,12 +170,12 @@ impl From for char { /// A symbology type. Refer to the [symbology documentation](https://docs.databento.com/api-reference-historical/basics/symbology) /// for more information. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] #[cfg_attr( feature = "python", + derive(strum::EnumIter), pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") )] -#[cfg_attr(feature = "python", derive(strum::EnumIter))] +#[repr(u8)] pub enum SType { /// Symbology using a unique numeric ID. InstrumentId = 0, @@ -222,12 +253,12 @@ pub mod rtype { /// A type of record, i.e. a struct implementing [`HasRType`](crate::record::HasRType). #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)] - #[repr(u8)] #[cfg_attr( feature = "python", + derive(strum::EnumIter), pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") )] - #[cfg_attr(feature = "python", derive(strum::EnumIter))] + #[repr(u8)] pub enum RType { /// Denotes a market-by-price record with a book depth of 0 (used for the /// [`Trades`](super::Schema::Trades) schema). @@ -271,6 +302,21 @@ pub mod rtype { Statistics = 0x18, /// Denotes a market by order record. Mbo = 0xA0, + /// Denotes a consolidated best bid and offer record. + Cbbo = 0xB1, + /// Denotes a consolidated best bid and offer record subsampled on a one-second + /// interval. + Cbbo1S = 0xC0, + /// Denotes a consolidated best bid and offer record subsampled on a one-minute + /// interval. + Cbbo1M = 0xC1, + /// Denotes a consolidated best bid and offer trade record containing the + /// consolidated BBO before the trade. + Tcbbo = 0xC2, + /// Denotes a best bid and offer record subsampled on a one-second interval. + Bbo1S = 0xC3, + /// Denotes a best bid and offer record subsampled on a one-minute interval. + Bbo1M = 0xC4, } /// Denotes a market-by-price record with a book depth of 0 (used for the @@ -315,6 +361,18 @@ pub mod rtype { pub const STATISTICS: u8 = RType::Statistics as u8; /// Denotes a market-by-order record. pub const MBO: u8 = RType::Mbo as u8; + /// Denotes a consolidated best bid and offer record. + pub const CBBO: u8 = RType::Cbbo as u8; + /// Denotes a consolidated best bid and offer record subsampled on a one-second interval. + pub const CBBO_1S: u8 = RType::Cbbo1S as u8; + /// Denotes a consolidated best bid and offer record subsampled on a one-minute interval. + pub const CBBO_1M: u8 = RType::Cbbo1M as u8; + /// Denotes a consolidated best bid and offer trade record containing the consolidated BBO before the trade. + pub const TCBBO: u8 = RType::Tcbbo as u8; + /// Denotes a best bid and offer record subsampled on a one-second interval. + pub const BBO_1S: u8 = RType::Bbo1S as u8; + /// Denotes a best bid and offer record subsampled on a one-minute interval. + pub const BBO_1M: u8 = RType::Bbo1M as u8; /// Get the corresponding `rtype` for the given `schema`. impl From for RType { @@ -333,6 +391,12 @@ pub mod rtype { Schema::Statistics => RType::Statistics, Schema::Status => RType::Status, Schema::Imbalance => RType::Imbalance, + Schema::Cbbo => RType::Cbbo, + Schema::Cbbo1S => RType::Cbbo1S, + Schema::Cbbo1M => RType::Cbbo1M, + Schema::Tcbbo => RType::Tcbbo, + Schema::Bbo1S => RType::Bbo1S, + Schema::Bbo1M => RType::Bbo1M, } } } @@ -356,6 +420,12 @@ pub mod rtype { IMBALANCE => Some(Schema::Imbalance), STATISTICS => Some(Schema::Statistics), MBO => Some(Schema::Mbo), + CBBO => Some(Schema::Cbbo), + CBBO_1S => Some(Schema::Cbbo1S), + CBBO_1M => Some(Schema::Cbbo1M), + TCBBO => Some(Schema::Tcbbo), + BBO_1S => Some(Schema::Bbo1S), + BBO_1M => Some(Schema::Bbo1M), _ => None, } } @@ -382,6 +452,12 @@ pub mod rtype { "system" => Ok(RType::System), "statistics" => Ok(RType::Statistics), "mbo" => Ok(RType::Mbo), + "cbbo" => Ok(RType::Cbbo), + "cbbo-1s" => Ok(RType::Cbbo1S), + "cbbo-1m" => Ok(RType::Cbbo1M), + "tcbbo" => Ok(RType::Tcbbo), + "bbo-1s" => Ok(RType::Bbo1S), + "bbo-1m" => Ok(RType::Bbo1M), _ => Err(crate::Error::conversion::(s.to_owned())), } } @@ -408,6 +484,12 @@ pub mod rtype { RType::System => "system", RType::Statistics => "statistics", RType::Mbo => "mbo", + RType::Cbbo => "cbbo", + RType::Cbbo1S => "cbbo-1s", + RType::Cbbo1M => "cbbo-1m", + RType::Tcbbo => "tcbbo", + RType::Bbo1S => "bbo-1s", + RType::Bbo1M => "bbo-1m", } } } @@ -415,11 +497,14 @@ pub mod rtype { /// A data record schema. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u16)] -#[cfg_attr(feature = "python", pyo3::pyclass(module = "databento_dbn"))] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn") +)] #[cfg_attr(not(feature = "python"), derive(MockPyo3))] -#[cfg_attr(feature = "python", derive(strum::EnumIter))] #[cfg_attr(test, derive(strum::EnumCount))] +#[repr(u16)] pub enum Schema { /// Market by order. #[pyo3(name = "MBO")] @@ -465,10 +550,29 @@ pub enum Schema { /// trading session. #[pyo3(name = "OHLCV_EOD")] OhlcvEod = 13, + /// Consolidated best bid and offer. + #[pyo3(name = "CBBO")] + Cbbo = 14, + /// Consolidated best bid and offer subsampled at one-second intervals, in addition to trades. + #[pyo3(name = "CBBO_1S")] + Cbbo1S = 15, + /// Consolidated best bid and offer subsampled at one-minute intervals, in addition to trades. + #[pyo3(name = "CBBO_1M")] + Cbbo1M = 16, + /// All trade events with the consolidated best bid and offer (CBBO) immediately **before** the + /// effect of the trade. + #[pyo3(name = "TCBBO")] + Tcbbo = 17, + /// Best bid and offer subsampled at one-second intervals, in addition to trades. + #[pyo3(name = "BBO_1S")] + Bbo1S = 18, + /// Best bid and offer subsampled at one-minute intervals, in addition to trades. + #[pyo3(name = "BBO_1M")] + Bbo1M = 19, } /// The number of [`Schema`]s. -pub const SCHEMA_COUNT: usize = 14; +pub const SCHEMA_COUNT: usize = 20; impl std::str::FromStr for Schema { type Err = crate::Error; @@ -489,6 +593,12 @@ impl std::str::FromStr for Schema { "statistics" => Ok(Schema::Statistics), "status" => Ok(Schema::Status), "imbalance" => Ok(Schema::Imbalance), + "cbbo" => Ok(Schema::Cbbo), + "cbbo-1s" => Ok(Schema::Cbbo1S), + "cbbo-1m" => Ok(Schema::Cbbo1M), + "tcbbo" => Ok(Schema::Tcbbo), + "bbo-1s" => Ok(Schema::Bbo1S), + "bbo-1m" => Ok(Schema::Bbo1M), _ => Err(crate::Error::conversion::(s.to_owned())), } } @@ -518,6 +628,12 @@ impl Schema { Schema::Statistics => "statistics", Schema::Status => "status", Schema::Imbalance => "imbalance", + Schema::Cbbo => "cbbo", + Schema::Cbbo1S => "cbbo-1s", + Schema::Cbbo1M => "cbbo-1m", + Schema::Tcbbo => "tcbbo", + Schema::Bbo1S => "bbo-1s", + Schema::Bbo1M => "bbo-1m", } } } @@ -530,19 +646,18 @@ impl Display for Schema { /// A data encoding format. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[repr(u8)] -#[cfg_attr(feature = "python", pyo3::pyclass(module = "databento_dbn"))] -#[cfg_attr(not(feature = "python"), derive(MockPyo3))] -#[cfg_attr(feature = "python", derive(strum::EnumIter))] pub enum Encoding { /// Databento Binary Encoding. - #[pyo3(name = "DBN")] Dbn = 0, /// Comma-separated values. - #[pyo3(name = "CSV")] Csv = 1, /// JavaScript object notation. - #[pyo3(name = "JSON")] Json = 2, } @@ -584,10 +699,13 @@ impl Display for Encoding { /// A compression format or none if uncompressed. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TryFromPrimitive)] -#[repr(u8)] -#[cfg_attr(feature = "python", pyo3::pyclass(module = "databento_dbn"))] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn") +)] #[cfg_attr(not(feature = "python"), derive(MockPyo3))] -#[cfg_attr(feature = "python", derive(strum::EnumIter))] +#[repr(u8)] pub enum Compression { /// Uncompressed. #[pyo3(name = "NONE")] @@ -650,10 +768,15 @@ pub mod flags { } /// The type of [`InstrumentDefMsg`](crate::record::InstrumentDefMsg) update. -#[repr(u8)] +#[allow(clippy::manual_non_exhaustive)] // false positive #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter, strum::AsRefStr), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[allow(clippy::manual_non_exhaustive)] // false positive +#[repr(u8)] pub enum SecurityUpdateAction { /// A new instrument definition. Add = b'A', @@ -667,9 +790,14 @@ pub enum SecurityUpdateAction { } /// The type of statistic contained in a [`StatMsg`](crate::record::StatMsg). -#[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[non_exhaustive] +#[repr(u16)] pub enum StatType { /// The price of the first trade of an instrument. `price` will be set. OpeningPrice = 1, @@ -711,11 +839,22 @@ pub enum StatType { /// `price` will be set to the VWAP while `quantity` will be the traded /// volume. Vwap = 13, + /// The implied volatility associated with the settlement price. `price` will be set + /// with the standard precision. + Volatility = 14, + /// The option delta associated with the settlement price. `price` will be set with + /// the standard precision. + Delta = 15, } /// The type of [`StatMsg`](crate::record::StatMsg) update. -#[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] +#[repr(u8)] pub enum StatUpdateAction { /// A new statistic. New = 1, @@ -724,9 +863,14 @@ pub enum StatUpdateAction { } /// The primary enum for the type of [`StatusMsg`](crate::record::StatusMsg) update. -#[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive, Default)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[non_exhaustive] +#[repr(u16)] pub enum StatusAction { /// No change. #[default] @@ -766,9 +910,14 @@ pub enum StatusAction { /// The secondary enum for a [`StatusMsg`](crate::record::StatusMsg) update, explains /// the cause of a halt or other change in `action`. -#[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive, Default)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[non_exhaustive] +#[repr(u16)] pub enum StatusReason { /// No reason is given. #[default] @@ -800,7 +949,7 @@ pub enum StatusReason { NewIssue = 15, /// The status changed because an issue is available. IssueAvailable = 16, - /// The status changed because the issue was reviewed. + /// The status changed because the issue(s) were reviewed. IssuesReviewed = 17, /// The status changed because the filing requirements were satisfied. FilingReqsSatisfied = 18, @@ -845,9 +994,14 @@ pub enum StatusReason { } /// Further information about a status update. -#[repr(u16)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive, Default)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] #[non_exhaustive] +#[repr(u16)] pub enum TradingEvent { /// No additional information given. #[default] @@ -864,8 +1018,13 @@ pub enum TradingEvent { /// An enum for representing unknown, true, or false values. Equivalent to /// `Option` but with a human-readable repr. -#[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive, Default)] +#[cfg_attr( + feature = "python", + derive(strum::EnumIter), + pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") +)] +#[repr(u8)] pub enum TriState { /// The value is not applicable or not known. #[default] @@ -900,9 +1059,9 @@ impl From> for TriState { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr( feature = "python", + derive(strum::EnumIter), pyo3::pyclass(module = "databento_dbn", rename_all = "SCREAMING_SNAKE_CASE") )] -#[cfg_attr(feature = "python", derive(strum::EnumIter))] #[non_exhaustive] pub enum VersionUpgradePolicy { /// Decode data from previous versions as-is. diff --git a/rust/dbn/src/lib.rs b/rust/dbn/src/lib.rs index b11cabe..c5ff123 100644 --- a/rust/dbn/src/lib.rs +++ b/rust/dbn/src/lib.rs @@ -64,9 +64,10 @@ pub use crate::{ metadata::{MappingInterval, Metadata, MetadataBuilder, SymbolMapping}, publishers::{Dataset, Publisher, Venue}, record::{ - BidAskPair, ErrorMsg, HasRType, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, - OhlcvMsg, Record, RecordHeader, RecordMut, StatMsg, StatusMsg, SymbolMappingMsg, SystemMsg, - TbboMsg, TradeMsg, WithTsOut, + Bbo1MMsg, Bbo1SMsg, BidAskPair, Cbbo1MMsg, Cbbo1SMsg, CbboMsg, ConsolidatedBidAskPair, + ErrorMsg, HasRType, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, OhlcvMsg, + Record, RecordHeader, RecordMut, StatMsg, StatusMsg, SymbolMappingMsg, SystemMsg, TbboMsg, + TradeMsg, WithTsOut, }, record_enum::{RecordEnum, RecordRefEnum}, record_ref::RecordRef, diff --git a/rust/dbn/src/macros.rs b/rust/dbn/src/macros.rs index 4b2d865..6b132ee 100644 --- a/rust/dbn/src/macros.rs +++ b/rust/dbn/src/macros.rs @@ -17,7 +17,7 @@ macro_rules! rtype_dispatch_base { match $rec_ref.rtype() { Ok(rtype) => Ok(match rtype { RType::Mbp0 => $handler!(TradeMsg), - RType::Mbp1 => $handler!(Mbp1Msg), + RType::Mbp1 | RType::Bbo1S | RType::Bbo1M => $handler!(Mbp1Msg), RType::Mbp10 => $handler!(Mbp10Msg), #[allow(deprecated)] RType::OhlcvDeprecated @@ -60,6 +60,7 @@ macro_rules! rtype_dispatch_base { } RType::Statistics => $handler!(StatMsg), RType::Mbo => $handler!(MboMsg), + RType::Cbbo | RType::Cbbo1S | RType::Cbbo1M | RType::Tcbbo => $handler!(CbboMsg), }), Err(e) => Err(e), } @@ -74,7 +75,7 @@ macro_rules! schema_dispatch_base { use $crate::record::*; match $schema { Schema::Mbo => $handler!(MboMsg), - Schema::Mbp1 | Schema::Tbbo => $handler!(Mbp1Msg), + Schema::Mbp1 | Schema::Tbbo | Schema::Bbo1S | Schema::Bbo1M => $handler!(Mbp1Msg), Schema::Mbp10 => $handler!(Mbp10Msg), Schema::Trades => $handler!(TradeMsg), Schema::Ohlcv1D @@ -88,6 +89,9 @@ macro_rules! schema_dispatch_base { Schema::Statistics => $handler!(StatMsg), Schema::Status => $handler!(StatusMsg), Schema::Imbalance => $handler!(ImbalanceMsg), + Schema::Cbbo | Schema::Cbbo1S | Schema::Cbbo1M | Schema::Tcbbo => { + $handler!(CbboMsg) + } } }}; } diff --git a/rust/dbn/src/pretty.rs b/rust/dbn/src/pretty.rs index 5139d35..a21ec70 100644 --- a/rust/dbn/src/pretty.rs +++ b/rust/dbn/src/pretty.rs @@ -12,7 +12,7 @@ use crate::FIXED_PRICE_SCALE; #[repr(transparent)] pub struct Ts(pub u64); -/// A new type for formatting nanosecond UNIX timestamps. +/// A new type for formatting prices. #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct Px(pub i64); diff --git a/rust/dbn/src/publishers.rs b/rust/dbn/src/publishers.rs index 5f624ba..007e7a6 100644 --- a/rust/dbn/src/publishers.rs +++ b/rust/dbn/src/publishers.rs @@ -8,6 +8,7 @@ use crate::{Error, Result}; /// A trading execution venue. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[non_exhaustive] #[repr(u16)] pub enum Venue { /// CME Globex @@ -215,6 +216,7 @@ impl std::str::FromStr for Venue { /// A source of data. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[non_exhaustive] #[repr(u16)] pub enum Dataset { /// CME MDP 3.0 Market Data @@ -249,9 +251,11 @@ pub enum Dataset { MemxMemoir = 15, /// MIAX Pearl Depth EprlDom = 16, - /// FINRA/Nasdaq TRF + /// FINRA/Nasdaq TRF (DEPRECATED) + #[deprecated(since = "0.17.0")] FinnNls = 17, - /// FINRA/NYSE TRF + /// FINRA/NYSE TRF (DEPRECATED) + #[deprecated(since = "0.17.0")] FinyTrades = 18, /// OPRA Binary OpraPillar = 19, @@ -302,7 +306,9 @@ impl Dataset { Self::XcisTrades => "XCIS.TRADES", Self::MemxMemoir => "MEMX.MEMOIR", Self::EprlDom => "EPRL.DOM", + #[allow(deprecated)] Self::FinnNls => "FINN.NLS", + #[allow(deprecated)] Self::FinyTrades => "FINY.TRADES", Self::OpraPillar => "OPRA.PILLAR", Self::DbeqBasic => "DBEQ.BASIC", @@ -353,7 +359,9 @@ impl std::str::FromStr for Dataset { "XCIS.TRADES" => Ok(Self::XcisTrades), "MEMX.MEMOIR" => Ok(Self::MemxMemoir), "EPRL.DOM" => Ok(Self::EprlDom), + #[allow(deprecated)] "FINN.NLS" => Ok(Self::FinnNls), + #[allow(deprecated)] "FINY.TRADES" => Ok(Self::FinyTrades), "OPRA.PILLAR" => Ok(Self::OpraPillar), "DBEQ.BASIC" => Ok(Self::DbeqBasic), @@ -374,6 +382,7 @@ impl std::str::FromStr for Dataset { /// A specific Venue from a specific data source. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] +#[non_exhaustive] #[repr(u16)] pub enum Publisher { /// CME Globex MDP 3.0 @@ -409,11 +418,11 @@ pub enum Publisher { /// MIAX Pearl Depth EprlDomEprl = 16, /// FINRA/Nasdaq TRF Carteret - FinnNlsFinn = 17, + XnasNlsFinn = 17, /// FINRA/Nasdaq TRF Chicago - FinnNlsFinc = 18, + XnasNlsFinc = 18, /// FINRA/NYSE TRF - FinyTradesFiny = 19, + XnysTradesFiny = 19, /// OPRA - NYSE American OpraPillarAmxo = 20, /// OPRA - Boston Options Exchange @@ -561,9 +570,9 @@ impl Publisher { Self::XcisTradesXcis => "XCIS.TRADES.XCIS", Self::MemxMemoirMemx => "MEMX.MEMOIR.MEMX", Self::EprlDomEprl => "EPRL.DOM.EPRL", - Self::FinnNlsFinn => "FINN.NLS.FINN", - Self::FinnNlsFinc => "FINN.NLS.FINC", - Self::FinyTradesFiny => "FINY.TRADES.FINY", + Self::XnasNlsFinn => "XNAS.NLS.FINN", + Self::XnasNlsFinc => "XNAS.NLS.FINC", + Self::XnysTradesFiny => "XNYS.TRADES.FINY", Self::OpraPillarAmxo => "OPRA.PILLAR.AMXO", Self::OpraPillarXbox => "OPRA.PILLAR.XBOX", Self::OpraPillarXcbo => "OPRA.PILLAR.XCBO", @@ -647,9 +656,9 @@ impl Publisher { Self::XcisTradesXcis => Venue::Xcis, Self::MemxMemoirMemx => Venue::Memx, Self::EprlDomEprl => Venue::Eprl, - Self::FinnNlsFinn => Venue::Finn, - Self::FinnNlsFinc => Venue::Finc, - Self::FinyTradesFiny => Venue::Finy, + Self::XnasNlsFinn => Venue::Finn, + Self::XnasNlsFinc => Venue::Finc, + Self::XnysTradesFiny => Venue::Finy, Self::OpraPillarAmxo => Venue::Amxo, Self::OpraPillarXbox => Venue::Xbox, Self::OpraPillarXcbo => Venue::Xcbo, @@ -733,9 +742,9 @@ impl Publisher { Self::XcisTradesXcis => Dataset::XcisTrades, Self::MemxMemoirMemx => Dataset::MemxMemoir, Self::EprlDomEprl => Dataset::EprlDom, - Self::FinnNlsFinn => Dataset::FinnNls, - Self::FinnNlsFinc => Dataset::FinnNls, - Self::FinyTradesFiny => Dataset::FinyTrades, + Self::XnasNlsFinn => Dataset::XnasNls, + Self::XnasNlsFinc => Dataset::XnasNls, + Self::XnysTradesFiny => Dataset::XnysTrades, Self::OpraPillarAmxo => Dataset::OpraPillar, Self::OpraPillarXbox => Dataset::OpraPillar, Self::OpraPillarXcbo => Dataset::OpraPillar, @@ -821,9 +830,9 @@ impl Publisher { (Dataset::XcisTrades, Venue::Xcis) => Ok(Self::XcisTradesXcis), (Dataset::MemxMemoir, Venue::Memx) => Ok(Self::MemxMemoirMemx), (Dataset::EprlDom, Venue::Eprl) => Ok(Self::EprlDomEprl), - (Dataset::FinnNls, Venue::Finn) => Ok(Self::FinnNlsFinn), - (Dataset::FinnNls, Venue::Finc) => Ok(Self::FinnNlsFinc), - (Dataset::FinyTrades, Venue::Finy) => Ok(Self::FinyTradesFiny), + (Dataset::XnasNls, Venue::Finn) => Ok(Self::XnasNlsFinn), + (Dataset::XnasNls, Venue::Finc) => Ok(Self::XnasNlsFinc), + (Dataset::XnysTrades, Venue::Finy) => Ok(Self::XnysTradesFiny), (Dataset::OpraPillar, Venue::Amxo) => Ok(Self::OpraPillarAmxo), (Dataset::OpraPillar, Venue::Xbox) => Ok(Self::OpraPillarXbox), (Dataset::OpraPillar, Venue::Xcbo) => Ok(Self::OpraPillarXcbo), @@ -923,9 +932,9 @@ impl std::str::FromStr for Publisher { "XCIS.TRADES.XCIS" => Ok(Self::XcisTradesXcis), "MEMX.MEMOIR.MEMX" => Ok(Self::MemxMemoirMemx), "EPRL.DOM.EPRL" => Ok(Self::EprlDomEprl), - "FINN.NLS.FINN" => Ok(Self::FinnNlsFinn), - "FINN.NLS.FINC" => Ok(Self::FinnNlsFinc), - "FINY.TRADES.FINY" => Ok(Self::FinyTradesFiny), + "XNAS.NLS.FINN" => Ok(Self::XnasNlsFinn), + "XNAS.NLS.FINC" => Ok(Self::XnasNlsFinc), + "XNYS.TRADES.FINY" => Ok(Self::XnysTradesFiny), "OPRA.PILLAR.AMXO" => Ok(Self::OpraPillarAmxo), "OPRA.PILLAR.XBOX" => Ok(Self::OpraPillarXbox), "OPRA.PILLAR.XCBO" => Ok(Self::OpraPillarXcbo), diff --git a/rust/dbn/src/python.rs b/rust/dbn/src/python.rs index 114e9de..0d4e826 100644 --- a/rust/dbn/src/python.rs +++ b/rust/dbn/src/python.rs @@ -145,7 +145,7 @@ impl PyFieldDesc for [u8; N] { mod tests { use super::PyFieldDesc; use crate::{ - record::{InstrumentDefMsg, MboMsg, Mbp10Msg}, + record::{CbboMsg, InstrumentDefMsg, MboMsg, Mbp10Msg}, SYMBOL_CSTR_LEN, }; @@ -280,6 +280,72 @@ mod tests { assert_eq!(Mbp10Msg::ordered_fields(""), exp) } + #[test] + fn test_cbbo_dtypes() { + let dtypes = CbboMsg::field_dtypes(""); + let exp = with_record_header_dtype(vec![ + ("price".to_owned(), "i8".to_owned()), + ("size".to_owned(), "u4".to_owned()), + ("action".to_owned(), "S1".to_owned()), + ("side".to_owned(), "S1".to_owned()), + ("flags".to_owned(), "u1".to_owned()), + ("_reserved".to_owned(), "S1".to_owned()), + ("ts_recv".to_owned(), "u8".to_owned()), + ("ts_in_delta".to_owned(), "i4".to_owned()), + ("sequence".to_owned(), "u4".to_owned()), + ("bid_px_00".to_owned(), "i8".to_owned()), + ("ask_px_00".to_owned(), "i8".to_owned()), + ("bid_sz_00".to_owned(), "u4".to_owned()), + ("ask_sz_00".to_owned(), "u4".to_owned()), + ("bid_pb_00".to_owned(), "u2".to_owned()), + ("_reserved1_00".to_owned(), "S2".to_owned()), + ("ask_pb_00".to_owned(), "u2".to_owned()), + ("_reserved2_00".to_owned(), "S2".to_owned()), + ]); + assert_eq!(dtypes, exp); + } + + #[test] + fn test_cbbo_fields() { + let mut exp_price = vec!["price".to_owned()]; + exp_price.push(format!("bid_px_00")); + exp_price.push(format!("ask_px_00")); + assert_eq!(CbboMsg::price_fields(""), exp_price); + assert_eq!( + CbboMsg::hidden_fields(""), + vec!["length".to_owned(), "_reserved".to_owned()] + ); + assert_eq!( + CbboMsg::timestamp_fields(""), + vec!["ts_event".to_owned(), "ts_recv".to_owned()] + ); + } + + #[test] + fn test_cbbo_ordered() { + let exp = vec![ + "ts_recv".to_owned(), + "ts_event".to_owned(), + "rtype".to_owned(), + "publisher_id".to_owned(), + "instrument_id".to_owned(), + "action".to_owned(), + "side".to_owned(), + "price".to_owned(), + "size".to_owned(), + "flags".to_owned(), + "ts_in_delta".to_owned(), + "sequence".to_owned(), + "bid_px_00".to_owned(), + "ask_px_00".to_owned(), + "bid_sz_00".to_owned(), + "ask_sz_00".to_owned(), + "bid_pb_00".to_owned(), + "ask_pb_00".to_owned(), + ]; + assert_eq!(CbboMsg::ordered_fields(""), exp) + } + #[test] fn test_definition_dtypes() { let dtypes = InstrumentDefMsg::field_dtypes(""); diff --git a/rust/dbn/src/python/enums.rs b/rust/dbn/src/python/enums.rs index 0e43efe..b2e5a92 100644 --- a/rust/dbn/src/python/enums.rs +++ b/rust/dbn/src/python/enums.rs @@ -4,37 +4,285 @@ use pyo3::{prelude::*, pyclass::CompareOp, type_object::PyTypeInfo, types::PyTyp use crate::{ enums::{Compression, Encoding, SType, Schema, SecurityUpdateAction, UserDefinedInstrument}, - RType, + Action, InstrumentClass, MatchAlgorithm, RType, Side, StatType, StatusAction, StatusReason, + TradingEvent, TriState, VersionUpgradePolicy, }; use super::{to_val_err, EnumIterator, PyFieldDesc}; -impl<'source> FromPyObject<'source> for SecurityUpdateAction { - fn extract(ob: &'source PyAny) -> PyResult { - u8::extract(ob).and_then(|num| Self::try_from(num).map_err(to_val_err)) +#[pymethods] +impl Side { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __repr__(&self) -> String { + format!("", self.name(), self.value()) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn name(&self) -> String { + self.as_ref().to_ascii_uppercase() + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) } } -impl IntoPy for SecurityUpdateAction { - fn into_py(self, py: Python<'_>) -> PyObject { - (self as u8).into_py(py) +#[pymethods] +impl Action { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __repr__(&self) -> String { + format!("", self.name(), self.value()) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn name(&self) -> String { + self.as_ref().to_ascii_uppercase() + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) + } +} + +#[pymethods] +impl InstrumentClass { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) } } -impl<'source> FromPyObject<'source> for UserDefinedInstrument { - fn extract(ob: &'source PyAny) -> PyResult { - u8::extract(ob).and_then(|num| Self::try_from(num).map_err(to_val_err)) +#[pymethods] +impl MatchAlgorithm { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) } } -impl IntoPy for UserDefinedInstrument { - fn into_py(self, py: Python<'_>) -> PyObject { - (self as u8).into_py(py) +#[pymethods] +impl UserDefinedInstrument { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __repr__(&self) -> String { + format!( + "", + self.name(), + self.value() + ) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn name(&self) -> String { + self.as_ref().to_ascii_uppercase() + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) } } #[pymethods] -impl Compression { +impl SType { #[new] fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); @@ -50,7 +298,7 @@ impl Compression { } fn __repr__(&self) -> String { - format!("", self.name(), self.value(),) + format!("", self.name(), self.value(),) } fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { @@ -74,7 +322,6 @@ impl Compression { self.as_str() } - // No metaclass support with pyo3, so `for c in Compression: ...` isn't possible #[classmethod] fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { EnumIterator::new::(py) @@ -84,17 +331,17 @@ impl Compression { #[pyo3(name = "from_str")] fn py_from_str(_: &PyType, value: &PyAny) -> PyResult { let value_str: &str = value.str().and_then(|s| s.extract())?; - let tokenized = value_str.to_lowercase(); + let tokenized = value_str.replace('-', "_").to_lowercase(); Self::from_str(&tokenized).map_err(to_val_err) } } #[pymethods] -impl Encoding { +impl RType { #[new] fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value) + Self::py_from_str(t, value).or_else(|_| Self::py_from_int(t, value)) } fn __hash__(&self) -> isize { @@ -106,17 +353,20 @@ impl Encoding { } fn __repr__(&self) -> String { - format!("", self.name(), self.value(),) + format!("", self.name(), self.value(),) } fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { - let Ok(other_enum) = Self::py_from_str(Self::type_object(py), other) else { - return py.NotImplemented(); - }; - match op { - CompareOp::Eq => self.eq(&other_enum).into_py(py), - CompareOp::Ne => self.ne(&other_enum).into_py(py), - _ => py.NotImplemented(), + if let Ok(other_enum) = Self::py_from_str(Self::type_object(py), other) + .or_else(|_| Self::py_from_int(Self::type_object(py), other)) + { + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } else { + py.NotImplemented() } } @@ -126,8 +376,8 @@ impl Encoding { } #[getter] - fn value(&self) -> &'static str { - self.as_str() + fn value(&self) -> u8 { + *self as u8 } #[classmethod] @@ -139,9 +389,26 @@ impl Encoding { #[pyo3(name = "from_str")] fn py_from_str(_: &PyType, value: &PyAny) -> PyResult { let value_str: &str = value.str().and_then(|s| s.extract())?; - let tokenized = value_str.to_lowercase(); + let tokenized = value_str.replace('-', "_").to_lowercase(); Self::from_str(&tokenized).map_err(to_val_err) } + + #[classmethod] + #[pyo3(name = "from_int")] + fn py_from_int(_: &PyType, value: &PyAny) -> PyResult { + let value: u8 = value.extract()?; + Self::try_from(value).map_err(to_val_err) + } + + #[classmethod] + #[pyo3(name = "from_schema")] + fn py_from_schema(pytype: &PyType, value: &PyAny) -> PyResult { + let schema: Schema = value + .extract() + .or_else(|_| Schema::py_from_str(Schema::type_object(pytype.py()), value)) + .map_err(to_val_err)?; + Ok(Self::from(schema)) + } } #[pymethods] @@ -200,7 +467,7 @@ impl Schema { } #[pymethods] -impl SType { +impl Encoding { #[new] fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); @@ -216,7 +483,7 @@ impl SType { } fn __repr__(&self) -> String { - format!("", self.name(), self.value(),) + format!("", self.name(), self.value(),) } fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { @@ -249,17 +516,17 @@ impl SType { #[pyo3(name = "from_str")] fn py_from_str(_: &PyType, value: &PyAny) -> PyResult { let value_str: &str = value.str().and_then(|s| s.extract())?; - let tokenized = value_str.replace('-', "_").to_lowercase(); + let tokenized = value_str.to_lowercase(); Self::from_str(&tokenized).map_err(to_val_err) } } #[pymethods] -impl RType { +impl Compression { #[new] fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value).or_else(|_| Self::py_from_int(t, value)) + Self::py_from_str(t, value) } fn __hash__(&self) -> isize { @@ -271,20 +538,17 @@ impl RType { } fn __repr__(&self) -> String { - format!("", self.name(), self.value(),) + format!("", self.name(), self.value(),) } fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { - if let Ok(other_enum) = Self::py_from_str(Self::type_object(py), other) - .or_else(|_| Self::py_from_int(Self::type_object(py), other)) - { - match op { - CompareOp::Eq => self.eq(&other_enum).into_py(py), - CompareOp::Ne => self.ne(&other_enum).into_py(py), - _ => py.NotImplemented(), - } - } else { - py.NotImplemented() + let Ok(other_enum) = Self::py_from_str(Self::type_object(py), other) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), } } @@ -294,10 +558,11 @@ impl RType { } #[getter] - fn value(&self) -> u8 { - *self as u8 + fn value(&self) -> &'static str { + self.as_str() } + // No metaclass support with pyo3, so `for c in Compression: ...` isn't possible #[classmethod] fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { EnumIterator::new::(py) @@ -307,25 +572,272 @@ impl RType { #[pyo3(name = "from_str")] fn py_from_str(_: &PyType, value: &PyAny) -> PyResult { let value_str: &str = value.str().and_then(|s| s.extract())?; - let tokenized = value_str.replace('-', "_").to_lowercase(); + let tokenized = value_str.to_lowercase(); Self::from_str(&tokenized).map_err(to_val_err) } +} + +#[pymethods] +impl SecurityUpdateAction { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __repr__(&self) -> String { + format!("", self.name(), self.value()) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn name(&self) -> String { + self.as_ref().to_ascii_uppercase() + } + + #[getter] + fn value(&self) -> u16 { + *self as u16 + } #[classmethod] - #[pyo3(name = "from_int")] - fn py_from_int(_: &PyType, value: &PyAny) -> PyResult { - let value: u8 = value.extract()?; - Self::try_from(value).map_err(to_val_err) + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) } #[classmethod] - #[pyo3(name = "from_schema")] - fn py_from_schema(pytype: &PyType, value: &PyAny) -> PyResult { - let schema: Schema = value - .extract() - .or_else(|_| Schema::py_from_str(Schema::type_object(pytype.py()), value)) - .map_err(to_val_err)?; - Ok(Self::from(schema)) + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) + } +} + +#[pymethods] +impl StatType { + #[new] + fn py_new(value: &PyAny) -> PyResult { + let i = value.extract::().map_err(to_val_err)?; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> u16 { + *self as u16 + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } +} + +#[pymethods] +impl StatusAction { + #[new] + fn py_new(value: &PyAny) -> PyResult { + let i = value.extract::().map_err(to_val_err)?; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> u16 { + *self as u16 + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } +} + +#[pymethods] +impl StatusReason { + #[new] + fn py_new(value: &PyAny) -> PyResult { + let i = value.extract::().map_err(to_val_err)?; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> u16 { + *self as u16 + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } +} + +#[pymethods] +impl TradingEvent { + #[new] + fn py_new(value: &PyAny) -> PyResult { + let i = value.extract::().map_err(to_val_err)?; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[getter] + fn value(&self) -> u16 { + *self as u16 + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } +} + +#[pymethods] +impl TriState { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let Ok(i) = value.extract::() else { + let t = Self::type_object(py); + let c = value.extract::().map_err(to_val_err)?; + return Self::py_from_str(t, c); + }; + Self::try_from(i).map_err(to_val_err) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + format!("{}", *self as u8 as char) + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::().or_else(|_| Self::py_new(py, other)) else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + fn opt_bool(&self) -> Option { + Option::from(*self) + } + + #[getter] + fn value(&self) -> String { + self.__str__() + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, value: char) -> PyResult { + Self::try_from(value as u8).map_err(to_val_err) + } +} + +#[pymethods] +impl VersionUpgradePolicy { + fn __hash__(&self) -> isize { + *self as isize + } + + fn __richcmp__(&self, other: &PyAny, op: CompareOp, py: Python<'_>) -> Py { + let Ok(other_enum) = other.extract::() else { + return py.NotImplemented(); + }; + match op { + CompareOp::Eq => self.eq(&other_enum).into_py(py), + CompareOp::Ne => self.ne(&other_enum).into_py(py), + _ => py.NotImplemented(), + } + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) } } diff --git a/rust/dbn/src/python/record.rs b/rust/dbn/src/python/record.rs index 0782460..87d8f37 100644 --- a/rust/dbn/src/python/record.rs +++ b/rust/dbn/src/python/record.rs @@ -9,9 +9,9 @@ use pyo3::{ use crate::{ compat::{ErrorMsgV1, InstrumentDefMsgV1, SymbolMappingMsgV1, SystemMsgV1}, - record::str_to_c_chars, + record::{str_to_c_chars, CbboMsg, ConsolidatedBidAskPair}, rtype, BidAskPair, ErrorMsg, HasRType, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, - Mbp1Msg, OhlcvMsg, Record, RecordHeader, SType, SecurityUpdateAction, StatMsg, + Mbp1Msg, OhlcvMsg, Publisher, Record, RecordHeader, SType, SecurityUpdateAction, StatMsg, StatUpdateAction, StatusAction, StatusMsg, StatusReason, SymbolMappingMsg, SystemMsg, TradeMsg, TradingEvent, TriState, UserDefinedInstrument, WithTsOut, FIXED_PRICE_SCALE, UNDEF_ORDER_SIZE, UNDEF_PRICE, UNDEF_TIMESTAMP, @@ -211,6 +211,216 @@ impl BidAskPair { } } +#[pymethods] +impl CbboMsg { + #[new] + fn py_new( + rtype: u8, + publisher_id: u16, + instrument_id: u32, + ts_event: u64, + price: i64, + size: u32, + action: c_char, + side: c_char, + flags: u8, + ts_recv: u64, + ts_in_delta: i32, + sequence: u32, + levels: Option, + ) -> Self { + Self { + hd: RecordHeader::new::(rtype, publisher_id, instrument_id, ts_event), + price, + size, + action, + side, + flags, + _reserved: [0; 1], + ts_recv, + ts_in_delta, + sequence, + levels: [levels.unwrap_or_default()], + } + } + + fn __bytes__(&self) -> &[u8] { + self.as_ref() + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __repr__(&self) -> String { + format!("{self:?}") + } + + #[getter] + fn rtype(&self) -> u8 { + self.hd.rtype + } + + #[getter] + fn publisher_id(&self) -> u16 { + self.hd.publisher_id + } + + #[getter] + fn instrument_id(&self) -> u32 { + self.hd.instrument_id + } + + #[getter] + fn ts_event(&self) -> u64 { + self.hd.ts_event + } + + #[getter] + #[pyo3(name = "pretty_price")] + fn py_pretty_price(&self) -> f64 { + self.price as f64 / FIXED_PRICE_SCALE as f64 + } + + #[getter] + #[pyo3(name = "pretty_ts_event")] + fn py_pretty_ts_event(&self, py: Python<'_>) -> PyResult { + get_utc_nanosecond_timestamp(py, self.ts_event()) + } + + #[getter] + #[pyo3(name = "pretty_ts_recv")] + fn py_pretty_ts_recv(&self, py: Python<'_>) -> PyResult { + get_utc_nanosecond_timestamp(py, self.ts_recv) + } + + #[pyo3(name = "record_size")] + fn py_record_size(&self) -> usize { + self.record_size() + } + + #[classattr] + fn size_hint() -> PyResult { + Ok(mem::size_of::()) + } + + #[getter] + #[pyo3(name = "action")] + fn py_action(&self) -> char { + self.action as u8 as char + } + + #[getter] + #[pyo3(name = "side")] + fn py_side(&self) -> char { + self.side as u8 as char + } + + #[classattr] + #[pyo3(name = "_dtypes")] + fn py_dtypes() -> Vec<(String, String)> { + Self::field_dtypes("") + } + + #[classattr] + #[pyo3(name = "_price_fields")] + fn py_price_fields() -> Vec { + Self::price_fields("") + } + + #[classattr] + #[pyo3(name = "_timestamp_fields")] + fn py_timestamp_fields() -> Vec { + Self::timestamp_fields("") + } + + #[classattr] + #[pyo3(name = "_hidden_fields")] + fn py_hidden_fields() -> Vec { + Self::hidden_fields("") + } + + #[classattr] + #[pyo3(name = "_ordered_fields")] + fn py_ordered_fields() -> Vec { + Self::ordered_fields("") + } +} + +#[pymethods] +impl ConsolidatedBidAskPair { + #[new] + fn py_new( + bid_px: Option, + ask_px: Option, + bid_sz: Option, + ask_sz: Option, + bid_pb: Option, + ask_pb: Option, + ) -> Self { + Self { + bid_px: bid_px.unwrap_or(UNDEF_PRICE), + ask_px: ask_px.unwrap_or(UNDEF_PRICE), + bid_sz: bid_sz.unwrap_or_default(), + ask_sz: ask_sz.unwrap_or_default(), + bid_pb: bid_pb.unwrap_or_default(), + ask_pb: ask_pb.unwrap_or_default(), + _reserved1: [0; 2], + _reserved2: [0; 2], + } + } + + #[getter] + #[pyo3(name = "pretty_ask_px")] + fn py_pretty_ask_px(&self) -> f64 { + match self.ask_px { + UNDEF_PRICE => f64::NAN, + _ => self.ask_px as f64 / FIXED_PRICE_SCALE as f64, + } + } + + #[getter] + #[pyo3(name = "pretty_bid_px")] + fn py_pretty_bid_px(&self) -> f64 { + match self.bid_px { + UNDEF_PRICE => f64::NAN, + _ => self.bid_px as f64 / FIXED_PRICE_SCALE as f64, + } + } + + #[getter] + #[pyo3(name = "pretty_ask_pb")] + fn py_pretty_ask_pb(&self) -> Option { + Publisher::try_from(self.ask_pb) + .map(|pb| pb.as_str().to_owned()) + .ok() + } + + #[getter] + #[pyo3(name = "pretty_bid_pb")] + fn py_pretty_bid_pb(&self) -> Option { + Publisher::try_from(self.bid_pb) + .map(|pb| pb.as_str().to_owned()) + .ok() + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __repr__(&self) -> String { + format!("{self:?}") + } +} + #[pymethods] impl TradeMsg { #[new] @@ -2735,6 +2945,55 @@ impl PyFieldDesc for [BidAskPair; N] { } } +impl PyFieldDesc for [ConsolidatedBidAskPair; N] { + fn field_dtypes(_field_name: &str) -> Vec<(String, String)> { + let mut res = Vec::new(); + let field_dtypes = ConsolidatedBidAskPair::field_dtypes(""); + for level in 0..N { + let mut dtypes = field_dtypes.clone(); + for dtype in dtypes.iter_mut() { + dtype.0.push_str(&format!("_{level:02}")); + } + res.extend(dtypes); + } + res + } + + fn price_fields(_field_name: &str) -> Vec { + let mut res = Vec::new(); + let price_fields = ConsolidatedBidAskPair::price_fields(""); + for level in 0..N { + let mut fields = price_fields.clone(); + for field in fields.iter_mut() { + field.push_str(&format!("_{level:02}")); + } + res.extend(fields); + } + res + } + + fn ordered_fields(_field_name: &str) -> Vec { + let mut res = Vec::new(); + let ordered_fields = ConsolidatedBidAskPair::ordered_fields(""); + for level in 0..N { + let mut fields = ordered_fields.clone(); + for field in fields.iter_mut() { + field.push_str(&format!("_{level:02}")); + } + res.extend(fields); + } + res + } + + fn hidden_fields(_field_name: &str) -> Vec { + Vec::new() + } + + fn timestamp_fields(_field_name: &str) -> Vec { + Vec::new() + } +} + // `WithTsOut` is converted to a 2-tuple in Python impl>> IntoPy for WithTsOut { fn into_py(self, py: Python<'_>) -> PyObject { diff --git a/rust/dbn/src/record.rs b/rust/dbn/src/record.rs index c1031dd..2b0d10f 100644 --- a/rust/dbn/src/record.rs +++ b/rust/dbn/src/record.rs @@ -150,6 +150,46 @@ pub struct BidAskPair { pub ask_ct: u32, } +/// A price level consolidated from multiple venues. +#[repr(C)] +#[derive(Clone, JsonSerialize, RecordDebug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "trivial_copy", derive(Copy))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "python", + pyo3::pyclass(get_all, set_all, dict, module = "databento_dbn"), + derive(crate::macros::PyFieldDesc) +)] +#[cfg_attr(test, derive(type_layout::TypeLayout))] +pub struct ConsolidatedBidAskPair { + /// The bid price. + #[dbn(fixed_price)] + pub bid_px: i64, + /// The ask price. + #[dbn(fixed_price)] + pub ask_px: i64, + /// The bid size. + pub bid_sz: u32, + /// The ask size. + pub ask_sz: u32, + /// The bid publisher ID assigned by Databento, which denotes the dataset and venue. + #[dbn(fmt_method)] + pub bid_pb: u16, + // Reserved for later usage + #[dbn(skip)] + #[doc(hidden)] + #[cfg_attr(feature = "serde", serde(skip))] + pub _reserved1: [c_char; 2], + /// The ask publisher ID assigned by Databento, which denotes the dataset and venue. + #[dbn(fmt_method)] + pub ask_pb: u16, + // Reserved for later usage + #[dbn(skip)] + #[doc(hidden)] + #[cfg_attr(feature = "serde", serde(skip))] + pub _reserved2: [c_char; 2], +} + /// Market by price implementation with a book depth of 0. Equivalent to /// MBP-0. The record of the [`Trades`](crate::enums::Schema::Trades) schema. #[repr(C)] @@ -219,7 +259,7 @@ pub struct TradeMsg { )] #[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope #[cfg_attr(test, derive(type_layout::TypeLayout))] -#[dbn_record(rtype::MBP_1)] +#[dbn_record(rtype::MBP_1, rtype::BBO_1S, rtype::BBO_1M)] pub struct Mbp1Msg { /// The common header. #[pyo3(get)] @@ -326,8 +366,78 @@ pub struct Mbp10Msg { pub levels: [BidAskPair; 10], } +/// Consolidated market by price implementation with a known book depth of 1. The record of the +/// [`Cbbo`](crate::enums::Schema::Cbbo) schema. +#[repr(C)] +#[derive(Clone, CsvSerialize, JsonSerialize, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "trivial_copy", derive(Copy))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "python", + pyo3::pyclass(set_all, dict, module = "databento_dbn", name = "CbboMsg"), + derive(crate::macros::PyFieldDesc) +)] +#[cfg_attr(not(feature = "python"), derive(MockPyo3))] // bring `pyo3` attribute into scope +#[cfg_attr(test, derive(type_layout::TypeLayout))] +#[dbn_record(rtype::CBBO, rtype::CBBO_1S, rtype::CBBO_1M, rtype::TCBBO)] +pub struct CbboMsg { + /// The common header. + #[pyo3(get)] + pub hd: RecordHeader, + /// The order price expressed as a signed integer where every 1 unit + /// corresponds to 1e-9, i.e. 1/1,000,000,000 or 0.000000001. + #[dbn(fixed_price)] + #[pyo3(get)] + pub price: i64, + /// The order quantity. + #[pyo3(get)] + pub size: u32, + /// The event action. Can be **A**dd, **C**ancel, **M**odify, clea**R**, or + /// **T**rade. + #[dbn(c_char, encode_order(2))] + pub action: c_char, + /// The side that initiates the event. Can be **A**sk for a sell order (or sell + /// aggressor in a trade), **B**id for a buy order (or buy aggressor in a trade), or + /// **N**one where no side is specified by the original source. + #[dbn(c_char, encode_order(3))] + pub side: c_char, + /// A combination of packet end with matching engine status. See + /// [`enums::flags`](crate::enums::flags) for possible values. + #[dbn(fmt_binary)] + #[pyo3(get)] + pub flags: u8, + // Reserved for future usage. + #[doc(hidden)] + #[cfg_attr(feature = "serde", serde(skip))] + pub _reserved: [c_char; 1], + /// The capture-server-received timestamp expressed as number of nanoseconds since + /// the UNIX epoch. + #[dbn(encode_order(0), index_ts, unix_nanos)] + #[pyo3(get)] + pub ts_recv: u64, + /// The delta of `ts_recv - ts_exchange_send`, max 2 seconds. + #[pyo3(get)] + pub ts_in_delta: i32, + /// The message sequence number assigned at the venue. + #[pyo3(get)] + pub sequence: u32, + /// The top of the order book. + #[pyo3(get)] + pub levels: [ConsolidatedBidAskPair; 1], +} + /// The record of the [`Tbbo`](crate::enums::Schema::Tbbo) schema. pub type TbboMsg = Mbp1Msg; +/// The record of the [`Bbo1S`](crate::enums::Schema::Bbo1S) schema. +pub type Bbo1SMsg = Mbp1Msg; +/// The record of the [`Bbo1M`](crate::enums::Schema::Bbo1M) schema. +pub type Bbo1MMsg = Mbp1Msg; +/// The record of the [`Cbbo1S`](crate::enums::Schema::Cbbo1S) schema. +pub type Cbbo1SMsg = CbboMsg; +/// The record of the [`Cbbo1M`](crate::enums::Schema::Cbbo1M) schema. +pub type Cbbo1MMsg = CbboMsg; +/// The record of the [`Tcbbo`](crate::enums::Schema::Tcbbo) schema. +pub type TcbboMsg = CbboMsg; /// Open, high, low, close, and volume. The record of the following schemas: /// - [`Ohlcv1S`](crate::enums::Schema::Ohlcv1S) @@ -1012,6 +1122,7 @@ mod tests { use rstest::rstest; use type_layout::{Field, TypeLayout}; + use crate::Schema; use crate::UNDEF_TIMESTAMP; use super::*; @@ -1075,8 +1186,9 @@ mod tests { #[case::header(RecordHeader::default::(rtype::MBO), 16)] #[case::mbo(MboMsg::default(), 56)] #[case::ba_pair(BidAskPair::default(), 32)] - #[case::mbp1(Mbp1Msg::default(), mem::size_of::() + mem::size_of::())] + #[case::mbp1(Mbp1Msg::default_for_schema(Schema::Mbp1), mem::size_of::() + mem::size_of::())] #[case::mbp10(Mbp10Msg::default(), mem::size_of::() + mem::size_of::() * 10)] + #[case::cbbo(CbboMsg::default_for_schema(Schema::Cbbo), mem::size_of::())] #[case::trade(TradeMsg::default(), 48)] #[case::definition(InstrumentDefMsg::default(), 400)] #[case::status(StatusMsg::default(), 40)] @@ -1095,7 +1207,8 @@ mod tests { #[case::header(RecordHeader::default::(rtype::MBO))] #[case::mbo(MboMsg::default())] #[case::ba_pair(BidAskPair::default())] - #[case::mbp1(Mbp1Msg::default())] + #[case::mbp1(Mbp1Msg::default_for_schema(crate::Schema::Mbp1))] + #[case::cbbo(CbboMsg::default_for_schema(crate::Schema::Cbbo))] #[case::mbp10(Mbp10Msg::default())] #[case::trade(TradeMsg::default())] #[case::definition(InstrumentDefMsg::default())] diff --git a/rust/dbn/src/record/impl_default.rs b/rust/dbn/src/record/impl_default.rs index a4a5c05..e4291d5 100644 --- a/rust/dbn/src/record/impl_default.rs +++ b/rust/dbn/src/record/impl_default.rs @@ -47,6 +47,21 @@ impl Default for BidAskPair { } } +impl Default for ConsolidatedBidAskPair { + fn default() -> Self { + Self { + bid_px: UNDEF_PRICE, + ask_px: UNDEF_PRICE, + bid_sz: 0, + ask_sz: 0, + bid_pb: 0, + ask_pb: 0, + _reserved1: [0; 2], + _reserved2: [0; 2], + } + } +} + impl Default for TradeMsg { fn default() -> Self { Self { @@ -64,10 +79,11 @@ impl Default for TradeMsg { } } -impl Default for Mbp1Msg { - fn default() -> Self { +impl Mbp1Msg { + /// Creates a new default Mbp1Msg for the given `schema`. + pub fn default_for_schema(schema: Schema) -> Self { Self { - hd: RecordHeader::default::(rtype::MBP_1), + hd: RecordHeader::default::(RType::from(schema) as u8), price: UNDEF_PRICE, size: UNDEF_ORDER_SIZE, action: 0, @@ -82,6 +98,25 @@ impl Default for Mbp1Msg { } } +impl CbboMsg { + /// Creates a new default CbboMsg for the given `schema`. + pub fn default_for_schema(schema: Schema) -> Self { + Self { + hd: RecordHeader::default::(RType::from(schema) as u8), + price: UNDEF_PRICE, + size: UNDEF_ORDER_SIZE, + action: 0, + side: 0, + flags: 0, + _reserved: [0; 1], + ts_recv: UNDEF_TIMESTAMP, + ts_in_delta: 0, + sequence: 0, + levels: Default::default(), + } + } +} + impl Default for Mbp10Msg { fn default() -> Self { Self { @@ -179,7 +214,7 @@ impl Default for InstrumentDefMsg { strike_price_currency: Default::default(), instrument_class: 0, strike_price: UNDEF_PRICE, - match_algorithm: MatchAlgorithm::Fifo as c_char, + match_algorithm: MatchAlgorithm::Undefined as c_char, md_security_trading_status: u8::MAX, main_fraction: u8::MAX, price_display_format: u8::MAX, @@ -248,7 +283,7 @@ impl Default for InstrumentDefMsgV1 { strike_price_currency: Default::default(), instrument_class: 0, strike_price: UNDEF_PRICE, - match_algorithm: MatchAlgorithm::Fifo as c_char, + match_algorithm: MatchAlgorithm::Undefined as c_char, md_security_trading_status: u8::MAX, main_fraction: u8::MAX, price_display_format: u8::MAX, diff --git a/rust/dbn/src/record/methods.rs b/rust/dbn/src/record/methods.rs index f51fba8..990a83c 100644 --- a/rust/dbn/src/record/methods.rs +++ b/rust/dbn/src/record/methods.rs @@ -154,6 +154,63 @@ impl TradeMsg { } } +impl CbboMsg { + /// Tries to convert the raw `side` to an enum. + /// + /// # Errors + /// This function returns an error if the `side` field does not + /// contain a valid [`Side`]. + pub fn side(&self) -> crate::Result { + Side::try_from(self.side as u8) + .map_err(|_| Error::conversion::(format!("{:#04X}", self.side as u8))) + } + + /// Tries to convert the raw event action to an enum. + /// + /// # Errors + /// This function returns an error if the `action` field does not + /// contain a valid [`Action`]. + pub fn action(&self) -> crate::Result { + Action::try_from(self.action as u8) + .map_err(|_| Error::conversion::(format!("{:#04X}", self.action as u8))) + } + + /// Parses the raw capture-server-received timestamp into a datetime. Returns `None` + /// if `ts_recv` contains the sentinel for a null timestamp. + pub fn ts_recv(&self) -> Option { + ts_to_dt(self.ts_recv) + } + + /// Parses the raw `ts_in_delta`—the delta of `ts_recv - ts_exchange_send`—into a duration. + pub fn ts_in_delta(&self) -> time::Duration { + time::Duration::new(0, self.ts_in_delta) + } +} + +impl ConsolidatedBidAskPair { + /// Tries to convert the raw `bid_pb` into an enum which is useful for + /// exhaustive pattern matching. + /// + /// # Errors + /// This function returns an error if the `publisher_id` does not correspond with + /// any known [`Publisher`]. + pub fn bid_pb(&self) -> crate::Result { + Publisher::try_from(self.bid_pb) + .map_err(|_| crate::error::Error::conversion::(self.bid_pb)) + } + + /// Tries to convert the raw `ask_pb` into an enum which is useful for + /// exhaustive pattern matching. + /// + /// # Errors + /// This function returns an error if the `ask_pb` does not correspond with + /// any known [`Publisher`]. + pub fn ask_pb(&self) -> crate::Result { + Publisher::try_from(self.ask_pb) + .map_err(|_| crate::error::Error::conversion::(self.ask_pb)) + } +} + impl Mbp1Msg { /// Tries to convert the raw `side` to an enum. /// diff --git a/rust/dbn/src/record_enum.rs b/rust/dbn/src/record_enum.rs index c0e5651..3c75bf0 100644 --- a/rust/dbn/src/record_enum.rs +++ b/rust/dbn/src/record_enum.rs @@ -1,6 +1,7 @@ use crate::{ - Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, OhlcvMsg, RType, - Record, RecordMut, RecordRef, StatMsg, StatusMsg, SymbolMappingMsg, SystemMsg, TradeMsg, + record::CbboMsg, Error, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg, + OhlcvMsg, RType, Record, RecordMut, RecordRef, StatMsg, StatusMsg, SymbolMappingMsg, SystemMsg, + TradeMsg, }; /// An owned DBN record type of flexible type. @@ -30,6 +31,8 @@ pub enum RecordEnum { SymbolMapping(SymbolMappingMsg), /// A non-error message from the Databento Live Subscription Gateway (LSG). System(SystemMsg), + /// A consolidated best bid and offer message. + Cbbo(CbboMsg), } /// An immutable reference to a DBN record of flexible type. Unlike [`RecordRef`], this @@ -62,6 +65,8 @@ pub enum RecordRefEnum<'a> { /// A reference to a non-error message from the Databento Live Subscription Gateway /// (LSG). System(&'a SystemMsg), + /// A reference to a consolidated best bid and offer message. + Cbbo(&'a CbboMsg), } impl<'a> From<&'a RecordEnum> for RecordRefEnum<'a> { @@ -79,6 +84,7 @@ impl<'a> From<&'a RecordEnum> for RecordRefEnum<'a> { RecordEnum::Error(rec) => Self::Error(rec), RecordEnum::SymbolMapping(rec) => Self::SymbolMapping(rec), RecordEnum::System(rec) => Self::System(rec), + RecordEnum::Cbbo(rec) => Self::Cbbo(rec), } } } @@ -100,6 +106,7 @@ impl<'a> RecordRefEnum<'a> { Self::Error(rec) => RecordEnum::from((*rec).clone()), Self::SymbolMapping(rec) => RecordEnum::from((*rec).clone()), Self::System(rec) => RecordEnum::from((*rec).clone()), + Self::Cbbo(rec) => RecordEnum::from((*rec).clone()), } } } @@ -113,7 +120,9 @@ impl<'a> TryFrom> for RecordRefEnum<'a> { match rec_ref.rtype()? { RType::Mbo => RecordRefEnum::Mbo(rec_ref.get_unchecked()), RType::Mbp0 => RecordRefEnum::Trade(rec_ref.get_unchecked()), - RType::Mbp1 => RecordRefEnum::Mbp1(rec_ref.get_unchecked()), + RType::Mbp1 | RType::Bbo1S | RType::Bbo1M => { + RecordRefEnum::Mbp1(rec_ref.get_unchecked()) + } RType::Mbp10 => RecordRefEnum::Mbp10(rec_ref.get_unchecked()), RType::OhlcvDeprecated | RType::Ohlcv1S @@ -144,6 +153,9 @@ impl<'a> TryFrom> for RecordRefEnum<'a> { RecordRefEnum::SymbolMapping(rec_ref.get_unchecked()) } RType::System => RecordRefEnum::System(rec_ref.get_unchecked()), + RType::Cbbo | RType::Cbbo1S | RType::Cbbo1M | RType::Tcbbo => { + RecordRefEnum::Cbbo(rec_ref.get_unchecked()) + } } }) } @@ -269,6 +281,16 @@ impl<'a> From<&'a SystemMsg> for RecordRefEnum<'a> { Self::System(rec) } } +impl From for RecordEnum { + fn from(rec: CbboMsg) -> Self { + Self::Cbbo(rec) + } +} +impl<'a> From<&'a CbboMsg> for RecordRefEnum<'a> { + fn from(rec: &'a CbboMsg) -> Self { + Self::Cbbo(rec) + } +} impl Record for RecordEnum { fn header(&self) -> &crate::RecordHeader { @@ -285,6 +307,7 @@ impl Record for RecordEnum { RecordEnum::Error(rec) => rec.header(), RecordEnum::SymbolMapping(rec) => rec.header(), RecordEnum::System(rec) => rec.header(), + RecordEnum::Cbbo(rec) => rec.header(), } } @@ -302,6 +325,7 @@ impl Record for RecordEnum { RecordEnum::Error(rec) => rec.raw_index_ts(), RecordEnum::SymbolMapping(rec) => rec.raw_index_ts(), RecordEnum::System(rec) => rec.raw_index_ts(), + RecordEnum::Cbbo(rec) => rec.raw_index_ts(), } } } @@ -321,6 +345,7 @@ impl RecordMut for RecordEnum { RecordEnum::Error(rec) => rec.header_mut(), RecordEnum::SymbolMapping(rec) => rec.header_mut(), RecordEnum::System(rec) => rec.header_mut(), + RecordEnum::Cbbo(rec) => rec.header_mut(), } } } @@ -340,6 +365,7 @@ impl<'a> Record for RecordRefEnum<'a> { RecordRefEnum::Error(rec) => rec.header(), RecordRefEnum::SymbolMapping(rec) => rec.header(), RecordRefEnum::System(rec) => rec.header(), + RecordRefEnum::Cbbo(rec) => rec.header(), } } @@ -357,6 +383,7 @@ impl<'a> Record for RecordRefEnum<'a> { RecordRefEnum::Error(rec) => rec.raw_index_ts(), RecordRefEnum::SymbolMapping(rec) => rec.raw_index_ts(), RecordRefEnum::System(rec) => rec.raw_index_ts(), + RecordRefEnum::Cbbo(rec) => rec.raw_index_ts(), } } } diff --git a/rust/dbn/src/record_ref.rs b/rust/dbn/src/record_ref.rs index 4202e23..7632d1e 100644 --- a/rust/dbn/src/record_ref.rs +++ b/rust/dbn/src/record_ref.rs @@ -166,6 +166,7 @@ impl<'a> From<&'a RecordEnum> for RecordRef<'a> { RecordEnum::Error(rec) => Self::from(rec), RecordEnum::SymbolMapping(rec) => Self::from(rec), RecordEnum::System(rec) => Self::from(rec), + RecordEnum::Cbbo(rec) => Self::from(rec), } } } @@ -185,6 +186,7 @@ impl<'a> From> for RecordRef<'a> { RecordRefEnum::Error(rec) => Self::from(rec), RecordRefEnum::SymbolMapping(rec) => Self::from(rec), RecordRefEnum::System(rec) => Self::from(rec), + RecordRefEnum::Cbbo(rec) => Self::from(rec), } } } diff --git a/tests/data/test_data.cbbo.dbn b/tests/data/test_data.cbbo.dbn new file mode 100644 index 0000000..308dfaf Binary files /dev/null and b/tests/data/test_data.cbbo.dbn differ diff --git a/tests/data/test_data.cbbo.dbn.zst b/tests/data/test_data.cbbo.dbn.zst new file mode 100644 index 0000000..57bbfec Binary files /dev/null and b/tests/data/test_data.cbbo.dbn.zst differ