diff --git a/.gitignore b/.gitignore
index eb1f7e27fd20..8f8e933f25db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,9 @@
*.tar.gz*
*.zip
+*.dbn
+*.dbn.zst
+
.benchmarks*
.coverage*
.history*
@@ -34,6 +37,7 @@
.vscode/
/catalog/
+/examples/notebooks/catalog/
__pycache__
_build/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 926ebe70a264..bb30dd698831 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -73,7 +73,7 @@ repos:
types: [python]
- repo: https://github.com/psf/black
- rev: 24.1.1
+ rev: 24.2.0
hooks:
- id: black
types_or: [python, pyi]
@@ -82,7 +82,7 @@ repos:
exclude: "docs/_pygments/monokai.py"
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.2.1
+ rev: v0.2.2
hooks:
- id: ruff
args: ["--fix"]
diff --git a/RELEASES.md b/RELEASES.md
index 65b7c6cef449..a262066b72ab 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,3 +1,42 @@
+# NautilusTrader 1.188.0 Beta
+
+Released on TBD (UTC).
+
+### Enhancements
+- Added `managed` parameter to `subscribe_order_book_deltas`, default true to retain current behavior (if false then the data engine will not automatically manage a book)
+- Added `managed` parameter to `subscribe_order_book_snapshots`, default true to retain current behavior (if false then the data engine will not automatically manage a book)
+- Removed `interval_ms` 20 millisecond limitation for `subscribe_order_book_snapshots` (i.e. just needs to be positive), although we recommend you consider subscribing to deltas below 100 milliseconds
+- Ported `LiveClock` and `LiveTimer` implementations to Rust
+- Implemented `OrderBookDeltas` pickling
+- Implemented `AverageTrueRange` in Rust, thanks @rsmb7z
+
+### Breaking Changes
+None
+
+### Fixes
+- Fixed `TradeId` memory leak due assigning unique values to the `Ustr` global string cache (which are never freed for the lifetime of the program)
+- Fixed `TradeTick` size precision for pyo3 conversion (size precision was incorrectly price precision)
+- Fixed `RiskEngine` cash value check when selling (would previously divide quantity by price which is too much), thanks for reporting@AnthonyVince
+- Fixed FOK time in force behavior (allows fills beyond the top level, will cancel if cannot fill full size)
+- Fixed IOC time in force behavior (allows fills beyond the top level, will cancel any remaining after all fills are applied)
+- Fixed `LiveClock` timer behavior for small intervals causing next time to be less than now (timer then would not run)
+- Fixed log level filtering for `log_level_file` (bug introduced in v1.187.0), thanks @twitu
+- Fixed logging `print_config` config option (was not being passed through to the logging system)
+- Fixed logging timestamps for backtesting (static clock was not being incrementally set to individual `TimeEvent` timestamps)
+- Fixed account balance updates (fills from zero quantity `NETTING` positions will generate account balance updates)
+- Fixed `MessageBus` publishable types collection type (needed to be `tuple` not `set`)
+- Fixed `Controller` registration of components to ensure all active clocks are iterated correctly during backtests
+- Fixed `Equity` short selling for `CASH` accounts (will now reject)
+- Fixed `ActorFactory.create` JSON encoding (was missing the encoding hook)
+- Fixed `ImportableConfig.create` JSON encoding (was missing the encoding hook)
+- Fixed `ImportableStrategyConfig.create` JSON encoding (was missing the encoding hook)
+- Fixed `ExecAlgorithmFactory.create` JSON encoding (was missing the encoding hook)
+- Fixed `ControllerConfig` base class and docstring
+- Fixed Interactive Brokers historical bar data bug, thanks @benjaminsingleton
+- Fixed persistence `freeze_dict` function to handle `fs_storage_options`, thanks @dimitar-petrov
+
+---
+
# NautilusTrader 1.187.0 Beta
Released on 9th February 2024 (UTC).
diff --git a/examples/live/binance/binance_spot_orderbook_imbalance_rust.py b/examples/live/binance/binance_spot_orderbook_imbalance_rust.py
new file mode 100644
index 000000000000..b5c4aa0319ac
--- /dev/null
+++ b/examples/live/binance/binance_spot_orderbook_imbalance_rust.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+# -------------------------------------------------------------------------------------------------
+# Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+# https://nautechsystems.io
+#
+# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# -------------------------------------------------------------------------------------------------
+
+from decimal import Decimal
+
+from nautilus_trader.adapters.binance.common.enums import BinanceAccountType
+from nautilus_trader.adapters.binance.config import BinanceDataClientConfig
+from nautilus_trader.adapters.binance.config import BinanceExecClientConfig
+from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory
+from nautilus_trader.adapters.binance.factories import BinanceLiveExecClientFactory
+from nautilus_trader.config import CacheConfig
+from nautilus_trader.config import InstrumentProviderConfig
+from nautilus_trader.config import LiveExecEngineConfig
+from nautilus_trader.config import LoggingConfig
+from nautilus_trader.config import TradingNodeConfig
+from nautilus_trader.examples.strategies.orderbook_imbalance_rust import OrderBookImbalance
+from nautilus_trader.examples.strategies.orderbook_imbalance_rust import OrderBookImbalanceConfig
+from nautilus_trader.live.node import TradingNode
+from nautilus_trader.model.identifiers import InstrumentId
+from nautilus_trader.model.identifiers import TraderId
+
+
+# *** THIS IS A TEST STRATEGY WITH NO ALPHA ADVANTAGE WHATSOEVER. ***
+# *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. ***
+
+
+# Configure the trading node
+config_node = TradingNodeConfig(
+ trader_id=TraderId("TESTER-001"),
+ logging=LoggingConfig(
+ log_level="INFO",
+ # log_level_file="DEBUG",
+ # log_file_format="json",
+ ),
+ exec_engine=LiveExecEngineConfig(
+ reconciliation=True,
+ reconciliation_lookback_mins=1440,
+ filter_position_reports=True,
+ ),
+ cache=CacheConfig(
+ database=None,
+ timestamps_as_iso8601=True,
+ flush_on_start=False,
+ ),
+ # snapshot_orders=True,
+ # snapshot_positions=True,
+ # snapshot_positions_interval=5.0,
+ data_clients={
+ "BINANCE": BinanceDataClientConfig(
+ api_key=None, # 'BINANCE_API_KEY' env var
+ api_secret=None, # 'BINANCE_API_SECRET' env var
+ account_type=BinanceAccountType.SPOT,
+ base_url_http=None, # Override with custom endpoint
+ base_url_ws=None, # Override with custom endpoint
+ us=False, # If client is for Binance US
+ testnet=False, # If client uses the testnet
+ instrument_provider=InstrumentProviderConfig(load_all=True),
+ ),
+ },
+ exec_clients={
+ "BINANCE": BinanceExecClientConfig(
+ api_key=None, # 'BINANCE_API_KEY' env var
+ api_secret=None, # 'BINANCE_API_SECRET' env var
+ account_type=BinanceAccountType.SPOT,
+ base_url_http=None, # Override with custom endpoint
+ base_url_ws=None, # Override with custom endpoint
+ us=False, # If client is for Binance US
+ testnet=False, # If client uses the testnet
+ instrument_provider=InstrumentProviderConfig(load_all=True),
+ ),
+ },
+ timeout_connection=20.0,
+ timeout_reconciliation=10.0,
+ timeout_portfolio=10.0,
+ timeout_disconnection=10.0,
+ timeout_post_stop=5.0,
+)
+
+# Instantiate the node with a configuration
+node = TradingNode(config=config_node)
+
+# Configure your strategy
+strat_config = OrderBookImbalanceConfig(
+ instrument_id=InstrumentId.from_str("ETHUSDT.BINANCE"),
+ external_order_claims=[InstrumentId.from_str("ETHUSDT.BINANCE")],
+ max_trade_size=Decimal("0.010"),
+)
+
+# Instantiate your strategy
+strategy = OrderBookImbalance(config=strat_config)
+
+# Add your strategies and modules
+node.trader.add_strategy(strategy)
+
+# Register your client factories with the node (can take user defined factories)
+node.add_data_client_factory("BINANCE", BinanceLiveDataClientFactory)
+node.add_exec_client_factory("BINANCE", BinanceLiveExecClientFactory)
+node.build()
+
+
+# Stop and dispose of the node with SIGINT/CTRL+C
+if __name__ == "__main__":
+ try:
+ node.run()
+ finally:
+ node.dispose()
diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py
index 66ecd99af678..af2468651a8b 100644
--- a/examples/live/databento/databento_subscriber.py
+++ b/examples/live/databento/databento_subscriber.py
@@ -14,10 +14,10 @@
# limitations under the License.
# -------------------------------------------------------------------------------------------------
-from nautilus_trader.adapters.databento.config import DatabentoDataClientConfig
-from nautilus_trader.adapters.databento.constants import DATABENTO
-from nautilus_trader.adapters.databento.constants import DATABENTO_CLIENT_ID
-from nautilus_trader.adapters.databento.factories import DatabentoLiveDataClientFactory
+from nautilus_trader.adapters.databento import DATABENTO
+from nautilus_trader.adapters.databento import DATABENTO_CLIENT_ID
+from nautilus_trader.adapters.databento import DatabentoDataClientConfig
+from nautilus_trader.adapters.databento import DatabentoLiveDataClientFactory
from nautilus_trader.common.enums import LogColor
from nautilus_trader.config import InstrumentProviderConfig
from nautilus_trader.config import LiveExecEngineConfig
@@ -45,6 +45,7 @@
instrument_ids = [
InstrumentId.from_str("ESH4.GLBX"),
# InstrumentId.from_str("ESM4.GLBX"),
+ # InstrumentId.from_str("ESU4.GLBX"),
# InstrumentId.from_str("AAPL.XCHI"),
]
diff --git a/examples/notebooks/backtest_binance_orderbook.ipynb b/examples/notebooks/backtest_binance_orderbook.ipynb
index 61049317e49f..838402bef1a1 100644
--- a/examples/notebooks/backtest_binance_orderbook.ipynb
+++ b/examples/notebooks/backtest_binance_orderbook.ipynb
@@ -7,7 +7,7 @@
"source": [
"# Backtest on Binance OrderBook data\n",
"\n",
- "This example runs through how to setup the data catalog and a `BacktestNode` to backtest an `OrderBookImbalance` strategy or order book data. This example requires you bring your Binance own order book data.\n",
+ "This tutorial runs through how to setup the data catalog and a `BacktestNode` to backtest an `OrderBookImbalance` strategy or order book data. This example requires you bring your Binance own order book data.\n",
"\n",
"**Warning:**\n",
"\n",
@@ -41,14 +41,17 @@
"import pandas as pd\n",
"\n",
"from nautilus_trader.backtest.node import BacktestNode\n",
+ "from nautilus_trader.backtest.node import BacktestVenueConfig\n",
+ "from nautilus_trader.backtest.node import BacktestDataConfig\n",
+ "from nautilus_trader.backtest.node import BacktestRunConfig\n",
+ "from nautilus_trader.backtest.node import BacktestEngineConfig\n",
"from nautilus_trader.core.datetime import dt_to_unix_nanos\n",
- "from nautilus_trader.config import BacktestRunConfig, BacktestVenueConfig, BacktestDataConfig, BacktestEngineConfig\n",
"from nautilus_trader.config import ImportableStrategyConfig\n",
"from nautilus_trader.config import LoggingConfig\n",
"from nautilus_trader.examples.strategies.ema_cross import EMACross, EMACrossConfig\n",
"from nautilus_trader.model.data import OrderBookDelta\n",
"from nautilus_trader.persistence.loaders import BinanceOrderBookDeltaDataLoader\n",
- "from nautilus_trader.persistence.wranglers import OrderBookDeltaDataWranglerV2\n",
+ "from nautilus_trader.persistence.wranglers import OrderBookDeltaDataWrangler\n",
"from nautilus_trader.persistence.catalog import ParquetDataCatalog\n",
"from nautilus_trader.test_kit.providers import TestInstrumentProvider"
]
@@ -113,7 +116,7 @@
"source": [
"# Process deltas using a wrangler\n",
"BTCUSDT_BINANCE = TestInstrumentProvider.btcusdt_binance()\n",
- "wrangler = OrderBookDeltaDataWranglerV2(BTCUSDT_BINANCE)\n",
+ "wrangler = OrderBookDeltaDataWrangler(BTCUSDT_BINANCE)\n",
"\n",
"deltas = wrangler.process(df_snap)\n",
"deltas += wrangler.process(df_update)\n",
@@ -146,7 +149,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Write instrument and ticks to catalog (this currently takes a minute - investigating)\n",
+ "# Write instrument and ticks to catalog\n",
"catalog.write_data([BTCUSDT_BINANCE])\n",
"catalog.write_data(deltas)"
]
diff --git a/examples/notebooks/backtest_example.ipynb b/examples/notebooks/backtest_example.ipynb
index fbb499879a96..29788baf61b2 100644
--- a/examples/notebooks/backtest_example.ipynb
+++ b/examples/notebooks/backtest_example.ipynb
@@ -5,7 +5,7 @@
"id": "0",
"metadata": {},
"source": [
- "# Complete backtest using the data catalog and a BacktestNode (higher level)\n",
+ "# Complete backtest using the data catalog and a BacktestNode (high-level API)\n",
"\n",
"This example runs through how to setup the data catalog and a `BacktestNode` for a single 'one-shot' backtest run."
]
@@ -32,8 +32,11 @@
"import pandas as pd\n",
"\n",
"from nautilus_trader.backtest.node import BacktestNode\n",
+ "from nautilus_trader.backtest.node import BacktestVenueConfig\n",
+ "from nautilus_trader.backtest.node import BacktestDataConfig\n",
+ "from nautilus_trader.backtest.node import BacktestRunConfig\n",
+ "from nautilus_trader.backtest.node import BacktestEngineConfig\n",
"from nautilus_trader.core.datetime import dt_to_unix_nanos\n",
- "from nautilus_trader.config import BacktestRunConfig, BacktestVenueConfig, BacktestDataConfig, BacktestEngineConfig\n",
"from nautilus_trader.config import ImportableStrategyConfig\n",
"from nautilus_trader.config import LoggingConfig\n",
"from nautilus_trader.examples.strategies.ema_cross import EMACross, EMACrossConfig\n",
@@ -170,6 +173,14 @@
"source": [
"result"
]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10",
+ "metadata": {},
+ "outputs": [],
+ "source": []
}
],
"metadata": {
diff --git a/examples/notebooks/backtest_fx_usdjpy.ipynb b/examples/notebooks/backtest_fx_usdjpy.ipynb
index f03a5dbb2e46..d2c3242d840c 100644
--- a/examples/notebooks/backtest_fx_usdjpy.ipynb
+++ b/examples/notebooks/backtest_fx_usdjpy.ipynb
@@ -5,9 +5,9 @@
"id": "0",
"metadata": {},
"source": [
- "# Complete backtest using a wrangler and BacktestEngine (lower level)\n",
+ "# Complete backtest using a wrangler and BacktestEngine (low-level API)\n",
"\n",
- "This example runs through how to setup a `BacktestEngine` for a single 'one-shot' backtest run."
+ "This tutorial runs through how to setup a `BacktestEngine` for a single 'one-shot' backtest run."
]
},
{
diff --git a/examples/notebooks/databento_data_catalog.ipynb b/examples/notebooks/databento_data_catalog.ipynb
new file mode 100644
index 000000000000..a11079005e7c
--- /dev/null
+++ b/examples/notebooks/databento_data_catalog.ipynb
@@ -0,0 +1,445 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Databento data catalog"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1",
+ "metadata": {},
+ "source": [
+ "**Info:**\n",
+ "\n",
+ "
\n",
+ "This tutorial is currently a work in progress (WIP).\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {},
+ "source": [
+ "This tutorial will walk through how to setup a Nautilus Parquet data catalog with various Databento schemas.\n",
+ "\n",
+ "Prerequities:\n",
+ "- The `databento` Python client library should be installed to make data requests `pip install -U databento`\n",
+ "- A Databento account (there is a free tier)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3",
+ "metadata": {},
+ "source": [
+ "## Requesting data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {},
+ "source": [
+ "We'll use a Databento historical client for the rest of this tutorial. You can either initialize one by passing your Databento API key to the constructor, or implicitly use the `DATABENTO_API_KEY` environment variable (as shown)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import databento as db\n",
+ "\n",
+ "client = db.Historical() # This will use the DATABENTO_API_KEY environment variable (recommended best practice)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6",
+ "metadata": {},
+ "source": [
+ "**It's important to note that every historical streaming request from `timeseries.get_range` will incur a cost (even for the same data), therefore we need to:**\n",
+ "- Know and understand the cost prior to making a request\n",
+ "- Not make requests for the same data more than once (not efficient)\n",
+ "- Persist the responses to disk by writing zstd compressed DBN files (so that we don't have to request again)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7",
+ "metadata": {},
+ "source": [
+ "We can use a metadata [get_cost endpoint](https://docs.databento.com/api-reference-historical/metadata/metadata-get-cost?historical=python&live=python) from the Databento API to get a quote on the cost, prior to each request.\n",
+ "Each request sequence will first request the cost of the data, and then make a request only if the data doesn't already exist on disk.\n",
+ "\n",
+ "Note the response returned is in USD, displayed as fractional cents."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8",
+ "metadata": {},
+ "source": [
+ "The following request is only for a small amount of data (as used in this Medium article [Building high-frequency trading signals in Python with Databento and sklearn](https://databento.com/blog/hft-sklearn-python)), just to demonstrate the basic workflow. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pathlib import Path\n",
+ "from databento import DBNStore"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "10",
+ "metadata": {},
+ "source": [
+ "We'll prepare a directory for the raw Databento DBN format data, which we'll use for the rest of the tutorial."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "11",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "DATABENTO_DATA_DIR = Path(\"databento\")\n",
+ "DATABENTO_DATA_DIR.mkdir(exist_ok=True)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Request cost quote (USD) - this endpoint is 'free'\n",
+ "client.metadata.get_cost(\n",
+ " dataset=\"GLBX.MDP3\",\n",
+ " symbols=[\"ES.n.0\"],\n",
+ " stype_in=\"continuous\",\n",
+ " schema=\"mbp-10\",\n",
+ " start=\"2023-12-06T14:30:00\",\n",
+ " end=\"2023-12-06T20:30:00\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "13",
+ "metadata": {},
+ "source": [
+ "Use the historical API to request for the data used in the Medium article."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "14",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "path = DATABENTO_DATA_DIR / \"es-front-glbx-mbp10.dbn.zst\"\n",
+ "\n",
+ "if not path.exists():\n",
+ " # Request data\n",
+ " client.timeseries.get_range(\n",
+ " dataset=\"GLBX.MDP3\",\n",
+ " symbols=[\"ES.n.0\"],\n",
+ " stype_in=\"continuous\",\n",
+ " schema=\"mbp-10\",\n",
+ " start=\"2023-12-06T14:30:00\",\n",
+ " end=\"2023-12-06T20:30:00\",\n",
+ " path=path, # <--- Passing a `path` parameter will ensure the data is written to disk\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Inspect the data by reading from disk and convert to a pandas.DataFrame\n",
+ "data = DBNStore.from_file(path)\n",
+ "\n",
+ "df = data.to_df()\n",
+ "df"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "16",
+ "metadata": {},
+ "source": [
+ "## Write to data catalog"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import shutil\n",
+ "from pathlib import Path\n",
+ "\n",
+ "from nautilus_trader.adapters.databento.loaders import DatabentoDataLoader\n",
+ "from nautilus_trader.model.identifiers import InstrumentId\n",
+ "from nautilus_trader.persistence.catalog import ParquetDataCatalog"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "CATALOG_PATH = Path.cwd() / \"catalog\"\n",
+ "\n",
+ "# Clear if it already exists\n",
+ "if CATALOG_PATH.exists():\n",
+ " shutil.rmtree(CATALOG_PATH)\n",
+ "CATALOG_PATH.mkdir()\n",
+ "\n",
+ "# Create a catalog instance\n",
+ "catalog = ParquetDataCatalog(CATALOG_PATH)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "19",
+ "metadata": {},
+ "source": [
+ "Now that we've prepared the data catalog, we need a `DatabentoDataLoader` which we'll use to decode and load the data into Nautilus objects."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "20",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "loader = DatabentoDataLoader()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "21",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "path = DATABENTO_DATA_DIR / \"es-front-glbx-mbp10.dbn.zst\"\n",
+ "instrument_id = InstrumentId.from_str(\"ES.n.0\") # This should be the raw symbol (update)\n",
+ "\n",
+ "depth10 = loader.from_dbn_file(\n",
+ " path=path,\n",
+ " instrument_id=instrument_id, # Not required but makes data loading faster (symbology mapping not required)\n",
+ " as_legacy_cython=False, # This will load Rust pyo3 objects to write to the catalog (we could use legacy Cython objects, but this is slightly more efficient)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "22",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Write data to catalog (this takes ~20 seconds or ~250,000/second for writing MBP-10 at the moment)\n",
+ "catalog.write_data(depth10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "23",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Test reading from catalog\n",
+ "depths = catalog.order_book_depth10()\n",
+ "len(depths)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "24",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "markdown",
+ "id": "25",
+ "metadata": {},
+ "source": [
+ "## Preparing a month of AAPL trades"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "26",
+ "metadata": {},
+ "source": [
+ "Now we'll expand on this workflow by preparing a month of AAPL trades on the Nasdaq exchange using the Databento `trade` schema, which will translate to Nautilus `TradeTick` objects."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Request cost quote (USD) - this endpoint is 'free'\n",
+ "client.metadata.get_cost(\n",
+ " dataset=\"XNAS.ITCH\",\n",
+ " symbols=[\"AAPL\"],\n",
+ " schema=\"trades\",\n",
+ " start=\"2024-01\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "28",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "path = DATABENTO_DATA_DIR / \"aapl-xnas-202401.trades.dbn.zst\"\n",
+ "\n",
+ "if not path.exists():\n",
+ " # Request data\n",
+ " client.timeseries.get_range(\n",
+ " dataset=\"XNAS.ITCH\",\n",
+ " symbols=[\"AAPL\"],\n",
+ " schema=\"trades\",\n",
+ " start=\"2024-01\",\n",
+ " path=path, # <--- Passing a `path` parameter will ensure the data is written to disk\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Inspect the data by reading from disk and convert to a pandas.DataFrame\n",
+ "data = DBNStore.from_file(path)\n",
+ "\n",
+ "df = data.to_df()\n",
+ "df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "30",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "instrument_id = InstrumentId.from_str(\"AAPL.XNAS\") # Using the Nasdaq ISO 10383 MIC (Market Identifier Code) as the venue\n",
+ "\n",
+ "trades = loader.from_dbn_file(\n",
+ " path=path,\n",
+ " instrument_id=instrument_id, # Not required but makes data loading faster (symbology mapping not required)\n",
+ " as_legacy_cython=False, # This will load Rust pyo3 objects to write to the catalog (we could use legacy Cython objects, but this is slightly more efficient)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "31",
+ "metadata": {},
+ "source": [
+ "Here we'll organize our data in a file per month, this is a rather arbitrary choice and a file per day could be equally valid.\n",
+ "\n",
+ "It may also be a good idea to create a function which can return the correct `basename_template` value for a given chunk of data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "32",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Write data to catalog\n",
+ "catalog.write_data(trades, basename_template=\"2024-01\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "33",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "trades = catalog.trade_ticks([instrument_id])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "34",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "len(trades)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "35",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/notebooks/external_data_backtest.ipynb b/examples/notebooks/external_data_backtest.ipynb
index 709e0d127bb0..9e05a1b045b1 100644
--- a/examples/notebooks/external_data_backtest.ipynb
+++ b/examples/notebooks/external_data_backtest.ipynb
@@ -7,7 +7,7 @@
"source": [
"# Loading external data\n",
"\n",
- "This example demonstrates how to load external data into the `ParquetDataCatalog`, and then use this to run a one-shot backtest using a `BacktestNode`.\n",
+ "This tutorial demonstrates how to load external data into the `ParquetDataCatalog`, and then use this to run a one-shot backtest using a `BacktestNode`.\n",
"\n",
"**Warning:**\n",
"\n",
@@ -32,11 +32,15 @@
"import fsspec\n",
"import pandas as pd\n",
"from nautilus_trader.core.datetime import dt_to_unix_nanos\n",
- "from nautilus_trader.model.data import QuoteTick\n",
- "from nautilus_trader.model.objects import Price, Quantity\n",
- "from nautilus_trader.backtest.node import BacktestNode, BacktestVenueConfig, BacktestDataConfig, BacktestRunConfig, BacktestEngineConfig\n",
+ "from nautilus_trader.backtest.node import BacktestNode\n",
+ "from nautilus_trader.backtest.node import BacktestVenueConfig\n",
+ "from nautilus_trader.backtest.node import BacktestDataConfig\n",
+ "from nautilus_trader.backtest.node import BacktestRunConfig\n",
+ "from nautilus_trader.backtest.node import BacktestEngineConfig\n",
"from nautilus_trader.config import ImportableStrategyConfig\n",
+ "from nautilus_trader.model.data import QuoteTick\n",
"from nautilus_trader.model.data import BarType\n",
+ "from nautilus_trader.model.objects import Price, Quantity\n",
"from nautilus_trader.persistence.catalog import ParquetDataCatalog\n",
"from nautilus_trader.persistence.wranglers import QuoteTickDataWrangler\n",
"from nautilus_trader.test_kit.providers import CSVTickDataLoader\n",
@@ -109,7 +113,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Write instrument and ticks to catalog (this currently takes a minute - investigating)\n",
+ "# Write instrument and ticks to catalog\n",
"catalog.write_data([EURUSD])\n",
"catalog.write_data(ticks)"
]
diff --git a/examples/notebooks/parquet_explorer.ipynb b/examples/notebooks/parquet_explorer.ipynb
index 4a1ec6acf513..ff6e4b2c0a41 100644
--- a/examples/notebooks/parquet_explorer.ipynb
+++ b/examples/notebooks/parquet_explorer.ipynb
@@ -7,7 +7,7 @@
"source": [
"# Parquet Explorer\n",
"\n",
- "In this example, we'll explore some basic query operations on Parquet files written by Nautilus. We'll utilize both the `datafusio`n and `pyarrow` libraries.\n",
+ "This tutorial explores some basic query operations on Parquet files written by Nautilus. We'll utilize both the `datafusio`n and `pyarrow` libraries.\n",
"\n",
"Before proceeding, ensure that you have `datafusion` installed. If not, you can install it by running:\n",
"```bash\n",
diff --git a/examples/notebooks/quick_start.ipynb b/examples/notebooks/quick_start.ipynb
index 65e0671111ce..3a05d67bc0ee 100644
--- a/examples/notebooks/quick_start.ipynb
+++ b/examples/notebooks/quick_start.ipynb
@@ -7,9 +7,8 @@
"source": [
"# Quick Start\n",
"\n",
- "This guide explains how to get up and running with NautilusTrader backtesting with some\n",
- "FX data. The Nautilus maintainers have pre-loaded some test data using the standard Nautilus persistence \n",
- "format (Parquet) for this guide.\n",
+ "This tutorial steps through how to get up and running with NautilusTrader backtesting using FX data.\n",
+ "The Nautilus maintainers have pre-loaded some test data using the standard Nautilus persistence format (Parquet) for this guide.\n",
"\n",
"For more details on how to load data into Nautilus, see [Backtest Example]((https://docs.nautilustrader.io/guides/backtest_example.html) and [Loading External Data](https://docs.nautilustrader.io/guides/loading_external_data.html).)."
]
@@ -30,13 +29,17 @@
"\n",
"import fsspec\n",
"import pandas as pd\n",
- "from nautilus_trader.core.datetime import dt_to_unix_nanos\n",
- "from nautilus_trader.model.data import QuoteTick\n",
- "from nautilus_trader.model.objects import Price, Quantity\n",
"\n",
- "from nautilus_trader.backtest.node import BacktestNode, BacktestVenueConfig, BacktestDataConfig, BacktestRunConfig, BacktestEngineConfig\n",
+ "from nautilus_trader.backtest.node import BacktestNode\n",
+ "from nautilus_trader.backtest.node import BacktestVenueConfig\n",
+ "from nautilus_trader.backtest.node import BacktestDataConfig\n",
+ "from nautilus_trader.backtest.node import BacktestRunConfig\n",
+ "from nautilus_trader.backtest.node import BacktestEngineConfig\n",
"from nautilus_trader.config import ImportableStrategyConfig\n",
"from nautilus_trader.config import LoggingConfig\n",
+ "from nautilus_trader.core.datetime import dt_to_unix_nanos\n",
+ "from nautilus_trader.model.data import QuoteTick\n",
+ "from nautilus_trader.model.objects import Price, Quantity\n",
"from nautilus_trader.persistence.catalog import ParquetDataCatalog\n",
"from nautilus_trader.test_kit.providers import TestInstrumentProvider"
]
diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock
index 682cfd2ed0a1..1d7e7c542d0d 100644
--- a/nautilus_core/Cargo.lock
+++ b/nautilus_core/Cargo.lock
@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
-version = "0.7.7"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
@@ -30,9 +30,9 @@ dependencies = [
[[package]]
name = "ahash"
-version = "0.8.7"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
+checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f"
dependencies = [
"cfg-if",
"const-random",
@@ -101,9 +101,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
-version = "1.0.79"
+version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "arc-swap"
@@ -166,7 +166,7 @@ version = "50.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d390feeb7f21b78ec997a4081a025baef1e2e0d6069e181939b61864c9779609"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow-buffer",
"arrow-data",
"arrow-schema",
@@ -266,7 +266,7 @@ dependencies = [
"arrow-schema",
"chrono",
"half",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"lexical-core",
"num",
"serde",
@@ -294,7 +294,7 @@ version = "50.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007035e17ae09c4e8993e4cb8b5b96edf0afb927cd38e2dff27189b274d83dcf"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow-array",
"arrow-buffer",
"arrow-data",
@@ -318,7 +318,7 @@ version = "50.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce20973c1912de6514348e064829e50947e35977bb9d7fb637dc99ea9ffd78c"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow-array",
"arrow-buffer",
"arrow-data",
@@ -368,7 +368,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -420,7 +420,7 @@ dependencies = [
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
- "hyper 1.1.0",
+ "hyper 1.2.0",
"hyper-util",
"itoa",
"matchit",
@@ -576,7 +576,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
"syn_derive",
]
@@ -603,9 +603,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.14.0"
+version = "3.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+checksum = "a3b1be7772ee4501dba05acbe66bb1e8760f6a6c474a36035631638e4415f130"
[[package]]
name = "bytecheck"
@@ -695,11 +695,10 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.0.83"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+checksum = "7f9fa1897e4325be0d68d48df6aa1a71ac2ed4d27723887e7754192705350730"
dependencies = [
- "jobserver",
"libc",
]
@@ -717,9 +716,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
-version = "0.4.33"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
+checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -731,9 +730,9 @@ dependencies = [
[[package]]
name = "chrono-tz"
-version = "0.8.5"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7"
+checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e"
dependencies = [
"chrono",
"chrono-tz-build",
@@ -795,18 +794,18 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.0"
+version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
+checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.5.0"
+version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
+checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstyle",
"clap_lex 0.7.0",
@@ -847,8 +846,8 @@ version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c64043d6c7b7a4c58e39e7efccfdea7b93d885a795d0c054a69dbbf4dd52686"
dependencies = [
- "strum",
- "strum_macros",
+ "strum 0.25.0",
+ "strum_macros 0.25.3",
"unicode-width",
]
@@ -932,9 +931,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
-version = "1.3.2"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
@@ -948,7 +947,7 @@ dependencies = [
"anes",
"cast",
"ciborium",
- "clap 4.5.0",
+ "clap 4.5.1",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
@@ -1123,11 +1122,11 @@ dependencies = [
[[package]]
name = "datafusion"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4328f5467f76d890fe3f924362dbc3a838c6a733f762b32d87f9e0b7bef5fb49"
+checksum = "b2b360b692bf6c6d6e6b6dbaf41a3be0020daeceac0f406aed54c75331e50dbb"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow",
"arrow-array",
"arrow-ipc",
@@ -1141,6 +1140,7 @@ dependencies = [
"datafusion-common",
"datafusion-execution",
"datafusion-expr",
+ "datafusion-functions",
"datafusion-optimizer",
"datafusion-physical-expr",
"datafusion-physical-plan",
@@ -1150,7 +1150,7 @@ dependencies = [
"glob",
"half",
"hashbrown 0.14.3",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"itertools 0.12.1",
"log",
"num_cpus",
@@ -1171,11 +1171,11 @@ dependencies = [
[[package]]
name = "datafusion-common"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29a7752143b446db4a2cccd9a6517293c6b97e8c39e520ca43ccd07135a4f7e"
+checksum = "37f343ccc298f440e25aa38ff82678291a7acc24061c7370ba6c0ff5cc811412"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow",
"arrow-array",
"arrow-buffer",
@@ -1192,9 +1192,9 @@ dependencies = [
[[package]]
name = "datafusion-execution"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d447650af16e138c31237f53ddaef6dd4f92f0e2d3f2f35d190e16c214ca496"
+checksum = "3f9c93043081487e335399a21ebf8295626367a647ac5cb87d41d18afad7d0f7"
dependencies = [
"arrow",
"chrono",
@@ -1213,25 +1213,40 @@ dependencies = [
[[package]]
name = "datafusion-expr"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8d19598e48a498850fb79f97a9719b1f95e7deb64a7a06f93f313e8fa1d524b"
+checksum = "e204d89909e678846b6a95f156aafc1ee5b36cb6c9e37ec2e1449b078a38c818"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow",
"arrow-array",
"datafusion-common",
"paste",
"sqlparser",
- "strum",
- "strum_macros",
+ "strum 0.26.1",
+ "strum_macros 0.26.1",
+]
+
+[[package]]
+name = "datafusion-functions"
+version = "36.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f1c73f7801b2b8ba2297b3ad78ffcf6c1fc6b8171f502987eb9ad5cb244ee7"
+dependencies = [
+ "arrow",
+ "base64",
+ "datafusion-common",
+ "datafusion-execution",
+ "datafusion-expr",
+ "hex",
+ "log",
]
[[package]]
name = "datafusion-optimizer"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7feb0391f1fc75575acb95b74bfd276903dc37a5409fcebe160bc7ddff2010"
+checksum = "5ae27e07bf1f04d327be5c2a293470879801ab5535204dc3b16b062fda195496"
dependencies = [
"arrow",
"async-trait",
@@ -1247,26 +1262,28 @@ dependencies = [
[[package]]
name = "datafusion-physical-expr"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e911bca609c89a54e8f014777449d8290327414d3e10c57a3e3c2122e38878d0"
+checksum = "dde620cd9ef76a3bca9c754fb68854bd2349c49f55baf97e08001f9e967f6d6b"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow",
"arrow-array",
"arrow-buffer",
"arrow-ord",
"arrow-schema",
+ "arrow-string",
"base64",
"blake2",
"blake3",
"chrono",
"datafusion-common",
+ "datafusion-execution",
"datafusion-expr",
"half",
"hashbrown 0.14.3",
"hex",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"itertools 0.12.1",
"log",
"md-5",
@@ -1281,11 +1298,11 @@ dependencies = [
[[package]]
name = "datafusion-physical-plan"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e96b546b8a02e9c2ab35ac6420d511f12a4701950c1eb2e568c122b4fefb0be3"
+checksum = "9a4c75fba9ea99d64b2246cbd2fcae2e6fc973e6616b1015237a616036506dd4"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow",
"arrow-array",
"arrow-buffer",
@@ -1299,7 +1316,7 @@ dependencies = [
"futures",
"half",
"hashbrown 0.14.3",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"itertools 0.12.1",
"log",
"once_cell",
@@ -1312,9 +1329,9 @@ dependencies = [
[[package]]
name = "datafusion-sql"
-version = "35.0.0"
+version = "36.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d18d36f260bbbd63aafdb55339213a23d540d3419810575850ef0a798a6b768"
+checksum = "21474a95c3a62d113599d21b439fa15091b538bac06bd20be0bb2e7d22903c09"
dependencies = [
"arrow",
"arrow-schema",
@@ -1339,7 +1356,7 @@ dependencies = [
"pyo3",
"serde",
"streaming-iterator",
- "strum",
+ "strum 0.25.0",
"thiserror",
"time",
"tokio",
@@ -1355,7 +1372,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -1381,18 +1398,18 @@ dependencies = [
[[package]]
name = "derive_builder"
-version = "0.13.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "660047478bc508c0fde22c868991eec0c40a63e48d610befef466d48e2bee574"
+checksum = "8f59169f400d8087f238c5c0c7db6a28af18681717f3b623227d92f397e938c7"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
-version = "0.13.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b217e6dd1011a54d12f3b920a411b5abd44b1716ecfe94f5f2f2f7b52e08ab7"
+checksum = "a4ec317cc3e7ef0928b0ca6e4a634a4d6c001672ae210438cf114a83e56b018d"
dependencies = [
"darling",
"proc-macro2",
@@ -1402,9 +1419,9 @@ dependencies = [
[[package]]
name = "derive_builder_macro"
-version = "0.13.0"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5f77d7e20ac9153428f7ca14a88aba652adfc7a0ef0a06d654386310ef663b"
+checksum = "870368c3fb35b8031abb378861d4460f573b92238ec2152c927a21f77e3e0127"
dependencies = [
"derive_builder_core",
"syn 1.0.109",
@@ -1442,9 +1459,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "either"
-version = "1.9.0"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
dependencies = [
"serde",
]
@@ -1681,7 +1698,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -1765,7 +1782,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.11",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"slab",
"tokio",
"tokio-util",
@@ -1784,7 +1801,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 1.0.0",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"slab",
"tokio",
"tokio-util",
@@ -1808,7 +1825,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
- "ahash 0.7.7",
+ "ahash 0.7.8",
]
[[package]]
@@ -1817,7 +1834,7 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"allocator-api2",
]
@@ -1850,9 +1867,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
-version = "0.3.5"
+version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3"
+checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
[[package]]
name = "hex"
@@ -1987,9 +2004,9 @@ dependencies = [
[[package]]
name = "hyper"
-version = "1.1.0"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75"
+checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
@@ -2001,6 +2018,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
+ "smallvec",
"tokio",
]
@@ -2027,7 +2045,7 @@ dependencies = [
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
- "hyper 1.1.0",
+ "hyper 1.2.0",
"pin-project-lite",
"socket2 0.5.5",
"tokio",
@@ -2090,9 +2108,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.2.2"
+version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
+checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@@ -2118,12 +2136,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is-terminal"
-version = "0.4.10"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
- "hermit-abi 0.3.5",
- "rustix",
+ "hermit-abi 0.3.6",
+ "libc",
"windows-sys 0.52.0",
]
@@ -2151,15 +2169,6 @@ version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
-[[package]]
-name = "jobserver"
-version = "0.1.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
-dependencies = [
- "libc",
-]
-
[[package]]
name = "js-sys"
version = "0.3.68"
@@ -2413,7 +2422,7 @@ dependencies = [
[[package]]
name = "nautilus-accounting"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"cbindgen",
@@ -2429,14 +2438,14 @@ dependencies = [
[[package]]
name = "nautilus-adapters"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"chrono",
"criterion",
"databento",
"dbn",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"itoa",
"log",
"nautilus-common",
@@ -2460,7 +2469,7 @@ dependencies = [
[[package]]
name = "nautilus-backtest"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"cbindgen",
"nautilus-common",
@@ -2474,23 +2483,25 @@ dependencies = [
[[package]]
name = "nautilus-common"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"cbindgen",
"chrono",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"log",
"nautilus-core",
"nautilus-model",
"pyo3",
+ "pyo3-asyncio",
"redis",
"rstest",
"serde",
"serde_json",
- "strum",
+ "strum 0.26.1",
"sysinfo",
"tempfile",
+ "tokio",
"tracing",
"tracing-subscriber",
"ustr",
@@ -2498,7 +2509,7 @@ dependencies = [
[[package]]
name = "nautilus-core"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"cbindgen",
@@ -2517,19 +2528,19 @@ dependencies = [
[[package]]
name = "nautilus-indicators"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"nautilus-core",
"nautilus-model",
"pyo3",
"rstest",
- "strum",
+ "strum 0.26.1",
]
[[package]]
name = "nautilus-infrastructure"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"nautilus-common",
@@ -2544,7 +2555,7 @@ dependencies = [
[[package]]
name = "nautilus-model"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"cbindgen",
@@ -2554,7 +2565,7 @@ dependencies = [
"evalexpr",
"float-cmp",
"iai",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"nautilus-core",
"once_cell",
"pyo3",
@@ -2563,7 +2574,7 @@ dependencies = [
"rust_decimal_macros",
"serde",
"serde_json",
- "strum",
+ "strum 0.26.1",
"tabled",
"thiserror",
"thousands",
@@ -2572,7 +2583,7 @@ dependencies = [
[[package]]
name = "nautilus-network"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"axum",
@@ -2581,7 +2592,7 @@ dependencies = [
"futures",
"futures-util",
"http 1.0.0",
- "hyper 1.1.0",
+ "hyper 1.2.0",
"nautilus-core",
"nonzero_ext",
"pyo3",
@@ -2597,7 +2608,7 @@ dependencies = [
[[package]]
name = "nautilus-persistence"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"anyhow",
"binary-heap-plus",
@@ -2621,7 +2632,7 @@ dependencies = [
[[package]]
name = "nautilus-pyo3"
-version = "0.18.0"
+version = "0.19.0"
dependencies = [
"nautilus-accounting",
"nautilus-adapters",
@@ -2786,7 +2797,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
- "hermit-abi 0.3.5",
+ "hermit-abi 0.3.6",
"libc",
]
@@ -2808,7 +2819,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -2855,9 +2866,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "openssl"
-version = "0.10.63"
+version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
@@ -2876,7 +2887,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -2887,18 +2898,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
-version = "300.2.2+3.2.1"
+version = "300.2.3+3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bbfad0063610ac26ee79f7484739e2b07555a75c42453b89263830b5c8103bc"
+checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
-version = "0.9.99"
+version = "0.9.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae"
+checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
dependencies = [
"cc",
"libc",
@@ -2968,7 +2979,7 @@ version = "50.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "547b92ebf0c1177e3892f44c8f79757ee62e678d564a9834189725f2c5b7a750"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"arrow-array",
"arrow-buffer",
"arrow-cast",
@@ -3034,7 +3045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
]
[[package]]
@@ -3092,7 +3103,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -3130,9 +3141,9 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.29"
+version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "plotters"
@@ -3334,7 +3345,7 @@ dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -3346,7 +3357,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -3592,16 +3603,17 @@ dependencies = [
[[package]]
name = "ring"
-version = "0.17.7"
+version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
+checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
+ "cfg-if",
"getrandom",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -3700,15 +3712,15 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
- "syn 2.0.48",
+ "syn 2.0.50",
"unicode-ident",
]
[[package]]
name = "rust_decimal"
-version = "1.34.2"
+version = "1.34.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755392e1a2f77afd95580d3f0d0e94ac83eeeb7167552c9b5bca549e61a94d83"
+checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df"
dependencies = [
"arrayvec",
"borsh",
@@ -3765,7 +3777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [
"log",
- "ring 0.17.7",
+ "ring 0.17.8",
"rustls-webpki 0.101.7",
"sct",
]
@@ -3777,7 +3789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41"
dependencies = [
"log",
- "ring 0.17.7",
+ "ring 0.17.8",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
@@ -3807,9 +3819,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a716eb65e3158e90e17cd93d855216e27bde02745ab842f2cab4a39dba1bacf"
+checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7"
[[package]]
name = "rustls-webpki"
@@ -3827,7 +3839,7 @@ version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
- "ring 0.17.7",
+ "ring 0.17.8",
"untrusted 0.9.0",
]
@@ -3837,7 +3849,7 @@ version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
- "ring 0.17.7",
+ "ring 0.17.8",
"rustls-pki-types",
"untrusted 0.9.0",
]
@@ -3850,9 +3862,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
-version = "1.0.16"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "same-file"
@@ -3884,7 +3896,7 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
- "ring 0.17.7",
+ "ring 0.17.8",
"untrusted 0.9.0",
]
@@ -3919,9 +3931,9 @@ dependencies = [
[[package]]
name = "semver"
-version = "1.0.21"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "seq-macro"
@@ -3931,29 +3943,29 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
[[package]]
name = "serde"
-version = "1.0.196"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.196"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
name = "serde_json"
-version = "1.0.113"
+version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@@ -4151,9 +4163,9 @@ dependencies = [
[[package]]
name = "sqlparser"
-version = "0.41.0"
+version = "0.43.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc2c25a6c66789625ef164b4c7d2e548d627902280c13710d33da8222169964"
+checksum = "f95c4bae5aba7cd30bd506f7140026ade63cff5afd778af8854026f9606bf5d4"
dependencies = [
"log",
"sqlparser_derive",
@@ -4167,7 +4179,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4189,7 +4201,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"atoi",
"byteorder",
"bytes",
@@ -4205,7 +4217,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"log",
"memchr",
"once_cell",
@@ -4402,7 +4414,16 @@ version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
- "strum_macros",
+ "strum_macros 0.25.3",
+]
+
+[[package]]
+name = "strum"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
+dependencies = [
+ "strum_macros 0.26.1",
]
[[package]]
@@ -4415,7 +4436,20 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
- "syn 2.0.48",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 2.0.50",
]
[[package]]
@@ -4437,9 +4471,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.48"
+version = "2.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
dependencies = [
"proc-macro2",
"quote",
@@ -4455,7 +4489,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4532,9 +4566,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
-version = "0.12.13"
+version = "0.12.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
+checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tempfile"
@@ -4559,28 +4593,28 @@ dependencies = [
[[package]]
name = "textwrap"
-version = "0.16.0"
+version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
[[package]]
name = "thiserror"
-version = "1.0.56"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.56"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4591,9 +4625,9 @@ checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
[[package]]
name = "thread_local"
-version = "1.1.7"
+version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
@@ -4702,7 +4736,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4802,7 +4836,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
- "indexmap 2.2.2",
+ "indexmap 2.2.3",
"toml_datetime",
"winnow",
]
@@ -4855,7 +4889,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4929,7 +4963,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.21.0"
-source = "git+https://github.com/snapview/tungstenite-rs#2ee05d10803d95ad48b3ad03d9d9a03164060e76"
+source = "git+https://github.com/snapview/tungstenite-rs#0fa41973b4c075f5d4a9e03a82a26a301ca31ce9"
dependencies = [
"byteorder",
"bytes",
@@ -4974,7 +5008,7 @@ checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
@@ -4997,9 +5031,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
-version = "0.1.22"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
@@ -5063,7 +5097,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e904a2279a4a36d2356425bb20be271029cc650c335bc82af8bfae30085a3d0"
dependencies = [
- "ahash 0.8.7",
+ "ahash 0.8.9",
"byteorder",
"lazy_static",
"parking_lot",
@@ -5155,7 +5189,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
"wasm-bindgen-shared",
]
@@ -5189,7 +5223,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5422,9 +5456,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
-version = "0.5.39"
+version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
@@ -5474,7 +5508,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.48",
+ "syn 2.0.50",
]
[[package]]
diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml
index e1c218d63437..223295e3c128 100644
--- a/nautilus_core/Cargo.toml
+++ b/nautilus_core/Cargo.toml
@@ -17,17 +17,17 @@ members = [
[workspace.package]
rust-version = "1.76.0"
-version = "0.18.0"
+version = "0.19.0"
edition = "2021"
authors = ["Nautech Systems "]
description = "A high-performance algorithmic trading platform and event-driven backtester"
documentation = "https://docs.nautilustrader.io"
[workspace.dependencies]
-anyhow = "1.0.79"
-chrono = "0.4.33"
+anyhow = "1.0.80"
+chrono = "0.4.34"
futures = "0.3.30"
-indexmap = "2.2.2"
+indexmap = "2.2.3"
itoa = "1.0.10"
once_cell = "1.19.0"
log = { version = "0.4.20", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] }
@@ -36,12 +36,12 @@ pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attr
rand = "0.8.5"
redis = { version = "0.24.0", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] }
rmp-serde = "1.1.2"
-rust_decimal = "1.34.2"
+rust_decimal = "1.34.3"
rust_decimal_macros = "1.34.2"
-serde = { version = "1.0.196", features = ["derive"] }
-serde_json = "1.0.112"
-strum = { version = "0.25.0", features = ["derive"] }
-thiserror = "1.0.56"
+serde = { version = "1.0.197", features = ["derive"] }
+serde_json = "1.0.113"
+strum = { version = "0.26.1", features = ["derive"] }
+thiserror = "1.0.57"
thousands = "0.2.0"
tracing = "0.1.40"
tokio = { version = "1.36.0", features = ["full"] }
diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs
index 49cd2cdbe703..f4fb7f8ac0c6 100644
--- a/nautilus_core/adapters/src/databento/common.rs
+++ b/nautilus_core/adapters/src/databento/common.rs
@@ -16,28 +16,11 @@
use anyhow::Result;
use databento::historical::DateTimeRange;
use nautilus_core::time::UnixNanos;
-use nautilus_model::identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue};
use time::OffsetDateTime;
-use ustr::Ustr;
-
-use super::types::DatabentoPublisher;
pub const DATABENTO: &str = "DATABENTO";
pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS";
-#[must_use]
-pub fn nautilus_instrument_id_from_databento(
- raw_symbol: Ustr,
- publisher: &DatabentoPublisher,
-) -> InstrumentId {
- let symbol = Symbol { value: raw_symbol };
- let venue = Venue {
- value: Ustr::from(publisher.venue.as_str()),
- }; // TODO: Optimize
-
- InstrumentId::new(symbol, venue)
-}
-
pub fn get_date_time_range(start: UnixNanos, end: Option) -> Result {
match end {
Some(end) => Ok(DateTimeRange::from((
diff --git a/nautilus_core/adapters/src/databento/parsing.rs b/nautilus_core/adapters/src/databento/decode.rs
similarity index 81%
rename from nautilus_core/adapters/src/databento/parsing.rs
rename to nautilus_core/adapters/src/databento/decode.rs
index 7f17f7e838ef..f2d4ec752de0 100644
--- a/nautilus_core/adapters/src/databento/parsing.rs
+++ b/nautilus_core/adapters/src/databento/decode.rs
@@ -22,6 +22,7 @@ use std::{
use anyhow::{anyhow, bail, Result};
use databento::dbn;
+use dbn::Record;
use itoa;
use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, time::UnixNanos};
use nautilus_model::{
@@ -47,8 +48,6 @@ use nautilus_model::{
};
use ustr::Ustr;
-use super::{common::nautilus_instrument_id_from_databento, types::DatabentoPublisher};
-
const BAR_SPEC_1S: BarSpecification = BarSpecification {
step: 1,
aggregation: BarAggregation::Second,
@@ -144,7 +143,7 @@ pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option Result {
+pub fn decode_min_price_increment(value: i64, currency: Currency) -> Result {
match value {
0 | i64::MAX => Price::new(
10f64.powi(-i32::from(currency.precision)),
@@ -157,7 +156,7 @@ pub fn parse_min_price_increment(value: i64, currency: Currency) -> Result Result {
+pub unsafe fn raw_ptr_to_string(ptr: *const c_char) -> Result {
let c_str: &CStr = unsafe { CStr::from_ptr(ptr) };
let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?;
Ok(str_slice.to_owned())
@@ -166,13 +165,13 @@ pub unsafe fn parse_raw_ptr_to_string(ptr: *const c_char) -> Result {
/// # Safety
///
/// - Assumes `ptr` is a valid C string pointer.
-pub unsafe fn parse_raw_ptr_to_ustr(ptr: *const c_char) -> Result {
+pub unsafe fn raw_ptr_to_ustr(ptr: *const c_char) -> Result {
let c_str: &CStr = unsafe { CStr::from_ptr(ptr) };
let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?;
Ok(Ustr::from(str_slice))
}
-pub fn parse_equity_v1(
+pub fn decode_equity_v1(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
@@ -185,7 +184,7 @@ pub fn parse_equity_v1(
None, // No ISIN available yet
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Some(Quantity::new(record.min_lot_size_round_lot.into(), 0)?),
None, // TBD
None, // TBD
@@ -196,14 +195,14 @@ pub fn parse_equity_v1(
)
}
-pub fn parse_futures_contract_v1(
+pub fn decode_futures_contract_v1(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result {
let currency = Currency::USD(); // TODO: Temporary hard coding of US futures for now
- let cfi_str = unsafe { parse_raw_ptr_to_string(record.cfi.as_ptr())? };
- let underlying = unsafe { parse_raw_ptr_to_ustr(record.asset.as_ptr())? };
+ let cfi_str = unsafe { raw_ptr_to_string(record.cfi.as_ptr())? };
+ let underlying = unsafe { raw_ptr_to_ustr(record.asset.as_ptr())? };
let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?;
FuturesContract::new(
@@ -215,7 +214,7 @@ pub fn parse_futures_contract_v1(
record.expiration,
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Quantity::new(1.0, 0)?, // TBD
Quantity::new(1.0, 0)?, // TBD
None, // TBD
@@ -227,13 +226,13 @@ pub fn parse_futures_contract_v1(
)
}
-pub fn parse_options_contract_v1(
+pub fn decode_options_contract_v1(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result {
- let currency_str = unsafe { parse_raw_ptr_to_string(record.currency.as_ptr())? };
- let cfi_str = unsafe { parse_raw_ptr_to_string(record.cfi.as_ptr())? };
+ let currency_str = unsafe { raw_ptr_to_string(record.currency.as_ptr())? };
+ let cfi_str = unsafe { raw_ptr_to_string(record.cfi.as_ptr())? };
let asset_class_opt = match instrument_id.venue.value.as_str() {
"OPRA" => Some(AssetClass::Equity),
_ => {
@@ -241,7 +240,7 @@ pub fn parse_options_contract_v1(
asset_class
}
};
- let underlying = unsafe { parse_raw_ptr_to_ustr(record.underlying.as_ptr())? };
+ let underlying = unsafe { raw_ptr_to_ustr(record.underlying.as_ptr())? };
let currency = Currency::from_str(¤cy_str)?;
OptionsContract::new(
@@ -255,7 +254,7 @@ pub fn parse_options_contract_v1(
Price::from_raw(record.strike_price, currency.precision)?,
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Quantity::new(1.0, 0)?, // TBD
Quantity::new(1.0, 0)?, // TBD
None, // TBD
@@ -272,24 +271,29 @@ pub fn is_trade_msg(order_side: OrderSide, action: c_char) -> bool {
order_side == OrderSide::NoOrderSide || action as u8 as char == 'T'
}
-pub fn parse_mbo_msg(
+pub fn decode_mbo_msg(
record: &dbn::MboMsg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
+ include_trades: bool,
) -> Result<(Option, Option)> {
let side = parse_order_side(record.side);
if is_trade_msg(side, record.action) {
- let trade = TradeTick::new(
- instrument_id,
- Price::from_raw(record.price, price_precision)?,
- Quantity::from_raw(u64::from(record.size) * FIXED_SCALAR as u64, 0)?,
- parse_aggressor_side(record.side),
- TradeId::new(itoa::Buffer::new().format(record.sequence))?,
- record.ts_recv,
- ts_init,
- );
- return Ok((None, Some(trade)));
+ if include_trades {
+ let trade = TradeTick::new(
+ instrument_id,
+ Price::from_raw(record.price, price_precision)?,
+ Quantity::from_raw(u64::from(record.size) * FIXED_SCALAR as u64, 0)?,
+ parse_aggressor_side(record.side),
+ TradeId::new(itoa::Buffer::new().format(record.sequence))?,
+ record.ts_recv,
+ ts_init,
+ );
+ return Ok((None, Some(trade)));
+ } else {
+ return Ok((None, None));
+ }
};
let order = BookOrder::new(
@@ -312,7 +316,7 @@ pub fn parse_mbo_msg(
Ok((Some(delta), None))
}
-pub fn parse_trade_msg(
+pub fn decode_trade_msg(
record: &dbn::TradeMsg,
instrument_id: InstrumentId,
price_precision: u8,
@@ -331,11 +335,12 @@ pub fn parse_trade_msg(
Ok(trade)
}
-pub fn parse_mbp1_msg(
+pub fn decode_mbp1_msg(
record: &dbn::Mbp1Msg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
+ include_trades: bool,
) -> Result<(QuoteTick, Option)> {
let top_level = &record.levels[0];
let quote = QuoteTick::new(
@@ -348,8 +353,8 @@ pub fn parse_mbp1_msg(
ts_init,
)?;
- let trade = match record.action as u8 as char {
- 'T' => Some(TradeTick::new(
+ let maybe_trade = if include_trades && record.action as u8 as char == 'T' {
+ Some(TradeTick::new(
instrument_id,
Price::from_raw(record.price, price_precision)?,
Quantity::from_raw(u64::from(record.size) * FIXED_SCALAR as u64, 0)?,
@@ -357,14 +362,15 @@ pub fn parse_mbp1_msg(
TradeId::new(itoa::Buffer::new().format(record.sequence))?,
record.ts_recv,
ts_init,
- )),
- _ => None,
+ ))
+ } else {
+ None
};
- Ok((quote, trade))
+ Ok((quote, maybe_trade))
}
-pub fn parse_mbp10_msg(
+pub fn decode_mbp10_msg(
record: &dbn::Mbp10Msg,
instrument_id: InstrumentId,
price_precision: u8,
@@ -416,7 +422,7 @@ pub fn parse_mbp10_msg(
Ok(depth)
}
-pub fn parse_bar_type(record: &dbn::OhlcvMsg, instrument_id: InstrumentId) -> Result {
+pub fn decode_bar_type(record: &dbn::OhlcvMsg, instrument_id: InstrumentId) -> Result {
let bar_type = match record.hd.rtype {
32 => {
// ohlcv-1s
@@ -443,7 +449,7 @@ pub fn parse_bar_type(record: &dbn::OhlcvMsg, instrument_id: InstrumentId) -> Re
Ok(bar_type)
}
-pub fn parse_ts_event_adjustment(record: &dbn::OhlcvMsg) -> Result {
+pub fn decode_ts_event_adjustment(record: &dbn::OhlcvMsg) -> Result {
let adjustment = match record.hd.rtype {
32 => {
// ohlcv-1s
@@ -470,14 +476,14 @@ pub fn parse_ts_event_adjustment(record: &dbn::OhlcvMsg) -> Result {
Ok(adjustment)
}
-pub fn parse_ohlcv_msg(
+pub fn decode_ohlcv_msg(
record: &dbn::OhlcvMsg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
) -> Result {
- let bar_type = parse_bar_type(record, instrument_id)?;
- let ts_event_adjustment = parse_ts_event_adjustment(record)?;
+ let bar_type = decode_bar_type(record, instrument_id)?;
+ let ts_event_adjustment = decode_ts_event_adjustment(record)?;
// Adjust `ts_event` from open to close of bar
let ts_event = record.hd.ts_event;
@@ -497,13 +503,14 @@ pub fn parse_ohlcv_msg(
Ok(bar)
}
-pub fn parse_record(
+pub fn decode_record(
record: &dbn::RecordRef,
- rtype: dbn::RType,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: Option,
-) -> Result<(Data, Option)> {
+ include_trades: bool,
+) -> Result<(Option, Option)> {
+ let rtype = record.rtype().expect("Invalid `rtype`");
let result = match rtype {
dbn::RType::Mbo => {
let msg = record.get::().unwrap(); // SAFETY: RType known
@@ -511,10 +518,12 @@ pub fn parse_record(
Some(ts_init) => ts_init,
None => msg.ts_recv,
};
- let result = parse_mbo_msg(msg, instrument_id, price_precision, ts_init)?;
+ let result =
+ decode_mbo_msg(msg, instrument_id, price_precision, ts_init, include_trades)?;
match result {
- (Some(delta), None) => (Data::Delta(delta), None),
- (None, Some(trade)) => (Data::Trade(trade), None),
+ (Some(delta), None) => (Some(Data::Delta(delta)), None),
+ (None, Some(trade)) => (Some(Data::Trade(trade)), None),
+ (None, None) => (None, None),
_ => bail!("Invalid `MboMsg` parsing combination"),
}
}
@@ -524,8 +533,8 @@ pub fn parse_record(
Some(ts_init) => ts_init,
None => msg.ts_recv,
};
- let trade = parse_trade_msg(msg, instrument_id, price_precision, ts_init)?;
- (Data::Trade(trade), None)
+ let trade = decode_trade_msg(msg, instrument_id, price_precision, ts_init)?;
+ (Some(Data::Trade(trade)), None)
}
dbn::RType::Mbp1 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
@@ -533,10 +542,11 @@ pub fn parse_record(
Some(ts_init) => ts_init,
None => msg.ts_recv,
};
- let result = parse_mbp1_msg(msg, instrument_id, price_precision, ts_init)?;
+ let result =
+ decode_mbp1_msg(msg, instrument_id, price_precision, ts_init, include_trades)?;
match result {
- (quote, None) => (Data::Quote(quote), None),
- (quote, Some(trade)) => (Data::Quote(quote), Some(Data::Trade(trade))),
+ (quote, None) => (Some(Data::Quote(quote)), None),
+ (quote, Some(trade)) => (Some(Data::Quote(quote)), Some(Data::Trade(trade))),
}
}
dbn::RType::Mbp10 => {
@@ -545,8 +555,8 @@ pub fn parse_record(
Some(ts_init) => ts_init,
None => msg.ts_recv,
};
- let depth = parse_mbp10_msg(msg, instrument_id, price_precision, ts_init)?;
- (Data::Depth10(depth), None)
+ let depth = decode_mbp10_msg(msg, instrument_id, price_precision, ts_init)?;
+ (Some(Data::Depth10(depth)), None)
}
dbn::RType::Ohlcv1S
| dbn::RType::Ohlcv1M
@@ -558,8 +568,8 @@ pub fn parse_record(
Some(ts_init) => ts_init,
None => msg.hd.ts_event,
};
- let bar = parse_ohlcv_msg(msg, instrument_id, price_precision, ts_init)?;
- (Data::Bar(bar), None)
+ let bar = decode_ohlcv_msg(msg, instrument_id, price_precision, ts_init)?;
+ (Some(Data::Bar(bar)), None)
}
_ => bail!("RType {:?} is not currently supported", rtype),
};
@@ -567,22 +577,19 @@ pub fn parse_record(
Ok(result)
}
-pub fn parse_instrument_def_msg_v1(
+pub fn decode_instrument_def_msg_v1(
record: &dbn::compat::InstrumentDefMsgV1,
- publisher: &DatabentoPublisher,
+ instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result> {
- let raw_symbol = unsafe { parse_raw_ptr_to_ustr(record.raw_symbol.as_ptr())? };
- let instrument_id = nautilus_instrument_id_from_databento(raw_symbol, publisher);
-
match record.instrument_class as u8 as char {
- 'K' => Ok(Box::new(parse_equity_v1(record, instrument_id, ts_init)?)),
- 'F' => Ok(Box::new(parse_futures_contract_v1(
+ 'K' => Ok(Box::new(decode_equity_v1(record, instrument_id, ts_init)?)),
+ 'F' => Ok(Box::new(decode_futures_contract_v1(
record,
instrument_id,
ts_init,
)?)),
- 'C' | 'P' => Ok(Box::new(parse_options_contract_v1(
+ 'C' | 'P' => Ok(Box::new(decode_options_contract_v1(
record,
instrument_id,
ts_init,
@@ -599,22 +606,19 @@ pub fn parse_instrument_def_msg_v1(
}
}
-pub fn parse_instrument_def_msg(
+pub fn decode_instrument_def_msg(
record: &dbn::InstrumentDefMsg,
- publisher: &DatabentoPublisher,
+ instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result> {
- let raw_symbol = unsafe { parse_raw_ptr_to_ustr(record.raw_symbol.as_ptr())? };
- let instrument_id = nautilus_instrument_id_from_databento(raw_symbol, publisher);
-
match record.instrument_class as u8 as char {
- 'K' => Ok(Box::new(parse_equity(record, instrument_id, ts_init)?)),
- 'F' => Ok(Box::new(parse_futures_contract(
+ 'K' => Ok(Box::new(decode_equity(record, instrument_id, ts_init)?)),
+ 'F' => Ok(Box::new(decode_futures_contract(
record,
instrument_id,
ts_init,
)?)),
- 'C' | 'P' => Ok(Box::new(parse_options_contract(
+ 'C' | 'P' => Ok(Box::new(decode_options_contract(
record,
instrument_id,
ts_init,
@@ -631,7 +635,7 @@ pub fn parse_instrument_def_msg(
}
}
-pub fn parse_equity(
+pub fn decode_equity(
record: &dbn::InstrumentDefMsg,
instrument_id: InstrumentId,
ts_init: UnixNanos,
@@ -644,7 +648,7 @@ pub fn parse_equity(
None, // No ISIN available yet
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Some(Quantity::new(record.min_lot_size_round_lot.into(), 0)?),
None, // TBD
None, // TBD
@@ -655,14 +659,14 @@ pub fn parse_equity(
)
}
-pub fn parse_futures_contract(
+pub fn decode_futures_contract(
record: &dbn::InstrumentDefMsg,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result {
let currency = Currency::USD(); // TODO: Temporary hard coding of US futures for now
- let cfi_str = unsafe { parse_raw_ptr_to_string(record.cfi.as_ptr())? };
- let underlying = unsafe { parse_raw_ptr_to_ustr(record.asset.as_ptr())? };
+ let cfi_str = unsafe { raw_ptr_to_string(record.cfi.as_ptr())? };
+ let underlying = unsafe { raw_ptr_to_ustr(record.asset.as_ptr())? };
let (asset_class, _) = parse_cfi_iso10926(&cfi_str)?;
FuturesContract::new(
@@ -674,7 +678,7 @@ pub fn parse_futures_contract(
record.expiration,
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Quantity::new(1.0, 0)?, // TBD
Quantity::new(1.0, 0)?, // TBD
None, // TBD
@@ -686,13 +690,13 @@ pub fn parse_futures_contract(
)
}
-pub fn parse_options_contract(
+pub fn decode_options_contract(
record: &dbn::InstrumentDefMsg,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> Result {
- let currency_str = unsafe { parse_raw_ptr_to_string(record.currency.as_ptr())? };
- let cfi_str = unsafe { parse_raw_ptr_to_string(record.cfi.as_ptr())? };
+ let currency_str = unsafe { raw_ptr_to_string(record.currency.as_ptr())? };
+ let cfi_str = unsafe { raw_ptr_to_string(record.cfi.as_ptr())? };
let asset_class_opt = match instrument_id.venue.value.as_str() {
"OPRA" => Some(AssetClass::Equity),
_ => {
@@ -700,7 +704,7 @@ pub fn parse_options_contract(
asset_class
}
};
- let underlying = unsafe { parse_raw_ptr_to_ustr(record.underlying.as_ptr())? };
+ let underlying = unsafe { raw_ptr_to_ustr(record.underlying.as_ptr())? };
let currency = Currency::from_str(¤cy_str)?;
OptionsContract::new(
@@ -714,7 +718,7 @@ pub fn parse_options_contract(
Price::from_raw(record.strike_price, currency.precision)?,
currency,
currency.precision,
- parse_min_price_increment(record.min_price_increment, currency)?,
+ decode_min_price_increment(record.min_price_increment, currency)?,
Quantity::new(1.0, 0)?, // TBD
Quantity::new(1.0, 0)?, // TBD
None, // TBD
diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs
index 2b33f68d1043..a722ffc27883 100644
--- a/nautilus_core/adapters/src/databento/loader.rs
+++ b/nautilus_core/adapters/src/databento/loader.rs
@@ -35,10 +35,8 @@ use time;
use ustr::Ustr;
use super::{
- parsing::{parse_instrument_def_msg_v1, parse_record},
- types::DatabentoPublisher,
- types::Dataset,
- types::PublisherId,
+ decode::{decode_instrument_def_msg_v1, decode_record, raw_ptr_to_ustr},
+ types::{DatabentoPublisher, Dataset, PublisherId},
};
/// Provides a Nautilus data loader for Databento Binary Encoding (DBN) format data.
@@ -57,10 +55,6 @@ use super::{
/// - IMBALANCE -> `DatabentoImbalance`
/// - STATISTICS -> `DatabentoStatistics`
///
-/// For the loader to work correctly, you must first either:
-/// - Load Databento instrument definitions from a DBN file using `load_instruments(...)`
-/// - Manually add Nautilus instrument objects through `add_instruments(...)`
-///
/// # Warnings
/// The following Databento instrument classes are not supported:
/// - ``FUTURE_SPREAD``
@@ -75,15 +69,17 @@ use super::{
pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento")
)]
pub struct DatabentoDataLoader {
- publishers: IndexMap,
- venue_dataset: IndexMap,
+ publishers_map: IndexMap,
+ venue_dataset_map: IndexMap,
+ publisher_venue_map: IndexMap,
}
impl DatabentoDataLoader {
pub fn new(path: Option) -> Result {
let mut loader = Self {
- publishers: IndexMap::new(),
- venue_dataset: IndexMap::new(),
+ publishers_map: IndexMap::new(),
+ venue_dataset_map: IndexMap::new(),
+ publisher_venue_map: IndexMap::new(),
};
// Load publishers
@@ -108,13 +104,13 @@ impl DatabentoDataLoader {
let file_content = fs::read_to_string(path)?;
let publishers: Vec = serde_json::from_str(&file_content)?;
- self.publishers = publishers
+ self.publishers_map = publishers
.clone()
.into_iter()
.map(|p| (p.publisher_id, p))
.collect::>();
- self.venue_dataset = publishers
+ self.venue_dataset_map = publishers
.iter()
.map(|p| {
(
@@ -124,42 +120,54 @@ impl DatabentoDataLoader {
})
.collect::>();
+ self.publisher_venue_map = publishers
+ .into_iter()
+ .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
+ .collect::>();
+
Ok(())
}
/// Return the internal Databento publishers currently held by the loader.
#[must_use]
pub fn get_publishers(&self) -> &IndexMap {
- &self.publishers
+ &self.publishers_map
}
// Return the dataset which matches the given `venue` (if found).
#[must_use]
pub fn get_dataset_for_venue(&self, venue: &Venue) -> Option<&Dataset> {
- self.venue_dataset.get(venue)
+ self.venue_dataset_map.get(venue)
+ }
+
+ // Return the venue which matches the given `publisher_id` (if found).
+ #[must_use]
+ pub fn get_venue_for_publisher(&self, publisher_id: PublisherId) -> Option<&Venue> {
+ self.publisher_venue_map.get(&publisher_id)
}
pub fn get_nautilus_instrument_id_for_record(
&self,
record: &dbn::RecordRef,
metadata: &dbn::Metadata,
+ venue: Venue,
) -> Result {
- let (publisher_id, instrument_id, nanoseconds) = match record.rtype()? {
+ let (instrument_id, nanoseconds) = match record.rtype()? {
dbn::RType::Mbo => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp0 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp1 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp10 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Ohlcv1S
| dbn::RType::Ohlcv1M
@@ -167,7 +175,7 @@ impl DatabentoDataLoader {
| dbn::RType::Ohlcv1D
| dbn::RType::OhlcvEod => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.hd.ts_event)
+ (msg.hd.instrument_id, msg.hd.ts_event)
}
_ => bail!("RType is currently unsupported by NautilusTrader"),
};
@@ -175,20 +183,14 @@ impl DatabentoDataLoader {
let duration = time::Duration::nanoseconds(nanoseconds as i64);
let datetime = time::OffsetDateTime::UNIX_EPOCH
.checked_add(duration)
- .unwrap();
+ .unwrap(); // SAFETY: Relying on correctness of record timestamps
let date = datetime.date();
let symbol_map = metadata.symbol_map_for_date(date)?;
let raw_symbol = symbol_map
.get(instrument_id)
.expect("No raw symbol found for {instrument_id}");
- let symbol = Symbol {
- value: Ustr::from(raw_symbol),
- };
- let venue_str = self.publishers.get(&publisher_id).unwrap().venue.as_str();
- let venue = Venue {
- value: Ustr::from(venue_str),
- };
+ let symbol = Symbol::from_str_unchecked(raw_symbol);
Ok(InstrumentId::new(symbol, venue))
}
@@ -203,7 +205,8 @@ impl DatabentoDataLoader {
&self,
path: PathBuf,
instrument_id: Option,
- ) -> Result)>> + '_>
+ include_trades: bool,
+ ) -> Result, Option)>> + '_>
where
T: dbn::Record + dbn::HasRType + 'static,
{
@@ -218,15 +221,27 @@ impl DatabentoDataLoader {
match dbn_stream.get() {
Some(record) => {
let rec_ref = dbn::RecordRef::from(record);
- let rtype = rec_ref.rtype().expect("Invalid `rtype` for data loading");
let instrument_id = match &instrument_id {
Some(id) => *id, // Copy
- None => self
- .get_nautilus_instrument_id_for_record(&rec_ref, &metadata)
- .expect("Error resolving symbology mapping for {rec_ref}"),
+ None => {
+ let publisher_id = rec_ref.publisher().expect("No publisher for record")
+ as PublisherId;
+ let venue = self
+ .publisher_venue_map
+ .get(&publisher_id)
+ .expect("`Venue` not found for `publisher_id`");
+ self.get_nautilus_instrument_id_for_record(&rec_ref, &metadata, *venue)
+ .expect("Error resolving symbology mapping for {rec_ref}")
+ }
};
- match parse_record(&rec_ref, rtype, instrument_id, price_precision, None) {
+ match decode_record(
+ &rec_ref,
+ instrument_id,
+ price_precision,
+ None,
+ include_trades,
+ ) {
Ok(data) => Some(Ok(data)),
Err(e) => Some(Err(e)),
}
@@ -251,9 +266,18 @@ impl DatabentoDataLoader {
let rec_ref = dbn::RecordRef::from(record);
let msg = rec_ref.get::().unwrap();
- let publisher = self.publishers.get(&msg.hd.publisher_id).unwrap();
+ let raw_symbol = unsafe {
+ raw_ptr_to_ustr(record.raw_symbol.as_ptr())
+ .expect("Error parsing `raw_symbol`")
+ };
+ let symbol = Symbol { value: raw_symbol };
+ let venue = self
+ .publisher_venue_map
+ .get(&msg.hd.publisher_id)
+ .expect("`Venue` not found `publisher_id`");
+ let instrument_id = InstrumentId::new(symbol, *venue);
- match parse_instrument_def_msg_v1(record, publisher, msg.ts_recv) {
+ match decode_instrument_def_msg_v1(record, instrument_id, msg.ts_recv) {
Ok(data) => Some(Ok(data)),
Err(e) => Some(Err(e)),
}
diff --git a/nautilus_core/adapters/src/databento/mod.rs b/nautilus_core/adapters/src/databento/mod.rs
index a024322f21e5..692f14361bb1 100644
--- a/nautilus_core/adapters/src/databento/mod.rs
+++ b/nautilus_core/adapters/src/databento/mod.rs
@@ -14,8 +14,8 @@
// -------------------------------------------------------------------------------------------------
pub mod common;
+pub mod decode;
pub mod loader;
-pub mod parsing;
pub mod symbology;
pub mod types;
diff --git a/nautilus_core/adapters/src/databento/python/parsing.rs b/nautilus_core/adapters/src/databento/python/decode.rs
similarity index 69%
rename from nautilus_core/adapters/src/databento/python/parsing.rs
rename to nautilus_core/adapters/src/databento/python/decode.rs
index c087261fe6f9..68d9e060ee8f 100644
--- a/nautilus_core/adapters/src/databento/python/parsing.rs
+++ b/nautilus_core/adapters/src/databento/python/decode.rs
@@ -24,81 +24,87 @@ use nautilus_model::{
};
use pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyTuple};
-use crate::databento::parsing::{
- parse_equity_v1, parse_futures_contract_v1, parse_mbo_msg, parse_mbp10_msg, parse_mbp1_msg,
- parse_options_contract_v1, parse_trade_msg,
+use crate::databento::decode::{
+ decode_equity_v1, decode_futures_contract_v1, decode_mbo_msg, decode_mbp10_msg,
+ decode_mbp1_msg, decode_options_contract_v1, decode_trade_msg,
};
#[pyfunction]
-#[pyo3(name = "parse_equity")]
-pub fn py_parse_equity(
+#[pyo3(name = "decode_equity")]
+pub fn py_decode_equity(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> PyResult {
- parse_equity_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
+ decode_equity_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
}
#[pyfunction]
-#[pyo3(name = "parse_futures_contract")]
-pub fn py_parse_futures_contract(
+#[pyo3(name = "decode_futures_contract")]
+pub fn py_decode_futures_contract(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> PyResult {
- parse_futures_contract_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
+ decode_futures_contract_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
}
#[pyfunction]
-#[pyo3(name = "parse_options_contract")]
-pub fn py_parse_options_contract(
+#[pyo3(name = "decode_options_contract")]
+pub fn py_decode_options_contract(
record: &dbn::compat::InstrumentDefMsgV1,
instrument_id: InstrumentId,
ts_init: UnixNanos,
) -> PyResult {
- parse_options_contract_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
+ decode_options_contract_v1(record, instrument_id, ts_init).map_err(to_pyvalue_err)
}
#[pyfunction]
-#[pyo3(name = "parse_mbo_msg")]
-pub fn py_parse_mbo_msg(
+#[pyo3(name = "decode_mbo_msg")]
+pub fn py_decode_mbo_msg(
py: Python,
record: &dbn::MboMsg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
) -> PyResult {
- let result = parse_mbo_msg(record, instrument_id, price_precision, ts_init);
+ let result = decode_mbo_msg(record, instrument_id, price_precision, ts_init, false);
match result {
- Ok((Some(delta), None)) => Ok(delta.into_py(py)),
- Ok((None, Some(trade))) => Ok(trade.into_py(py)),
+ Ok((Some(data), None)) => Ok(data.into_py(py)),
Err(e) => Err(to_pyvalue_err(e)),
_ => Err(PyRuntimeError::new_err("Error parsing MBO message")),
}
}
#[pyfunction]
-#[pyo3(name = "parse_trade_msg")]
-pub fn py_parse_trade_msg(
+#[pyo3(name = "decode_trade_msg")]
+pub fn py_decode_trade_msg(
record: &dbn::TradeMsg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
) -> PyResult {
- parse_trade_msg(record, instrument_id, price_precision, ts_init).map_err(to_pyvalue_err)
+ decode_trade_msg(record, instrument_id, price_precision, ts_init).map_err(to_pyvalue_err)
}
#[pyfunction]
-#[pyo3(name = "parse_mbp1_msg")]
-pub fn py_parse_mbp1_msg(
+#[pyo3(name = "decode_mbp1_msg")]
+pub fn py_decode_mbp1_msg(
py: Python,
record: &dbn::Mbp1Msg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
+ include_trades: bool,
) -> PyResult {
- let result = parse_mbp1_msg(record, instrument_id, price_precision, ts_init);
+ let result = decode_mbp1_msg(
+ record,
+ instrument_id,
+ price_precision,
+ ts_init,
+ include_trades,
+ );
match result {
Ok((quote, Some(trade))) => {
@@ -120,12 +126,12 @@ pub fn py_parse_mbp1_msg(
}
#[pyfunction]
-#[pyo3(name = "parse_mbp10_msg")]
-pub fn py_parse_mbp10_msg(
+#[pyo3(name = "decode_mbp10_msg")]
+pub fn py_decode_mbp10_msg(
record: &dbn::Mbp10Msg,
instrument_id: InstrumentId,
price_precision: u8,
ts_init: UnixNanos,
) -> PyResult {
- parse_mbp10_msg(record, instrument_id, price_precision, ts_init).map_err(to_pyvalue_err)
+ decode_mbp10_msg(record, instrument_id, price_precision, ts_init).map_err(to_pyvalue_err)
}
diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs
index f0a5a5415601..74d8a9a91ef2 100644
--- a/nautilus_core/adapters/src/databento/python/historical.rs
+++ b/nautilus_core/adapters/src/databento/python/historical.rs
@@ -16,7 +16,7 @@
use std::{fs, num::NonZeroU64, sync::Arc};
use databento::{self, historical::timeseries::GetRangeParams};
-use dbn::{self, Record, VersionUpgradePolicy};
+use dbn::{self, VersionUpgradePolicy};
use indexmap::IndexMap;
use nautilus_core::{
python::to_pyvalue_err,
@@ -25,6 +25,7 @@ use nautilus_core::{
use nautilus_model::{
data::{bar::Bar, quote::QuoteTick, trade::TradeTick, Data},
enums::BarAggregation,
+ identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue},
};
use pyo3::{
exceptions::PyException,
@@ -35,8 +36,8 @@ use tokio::sync::Mutex;
use crate::databento::{
common::get_date_time_range,
- parsing::{parse_instrument_def_msg, parse_record},
- symbology::parse_nautilus_instrument_id,
+ decode::{decode_instrument_def_msg, decode_record, raw_ptr_to_ustr},
+ symbology::decode_nautilus_instrument_id,
types::{DatabentoPublisher, PublisherId},
};
@@ -47,11 +48,11 @@ use super::loader::convert_instrument_to_pyobject;
pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento")
)]
pub struct DatabentoHistoricalClient {
- clock: &'static AtomicTime,
- inner: Arc>,
- publishers: Arc>,
#[pyo3(get)]
pub key: String,
+ clock: &'static AtomicTime,
+ inner: Arc>,
+ publisher_venue_map: Arc>,
}
#[pymethods]
@@ -67,15 +68,16 @@ impl DatabentoHistoricalClient {
let file_content = fs::read_to_string(publishers_path)?;
let publishers_vec: Vec =
serde_json::from_str(&file_content).map_err(to_pyvalue_err)?;
- let publishers = publishers_vec
+
+ let publisher_venue_map = publishers_vec
.into_iter()
- .map(|p| (p.publisher_id, p))
- .collect::>();
+ .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
+ .collect::>();
Ok(Self {
clock: get_atomic_clock_realtime(),
inner: Arc::new(Mutex::new(client)),
- publishers: Arc::new(publishers),
+ publisher_venue_map: Arc::new(publisher_venue_map),
key,
})
}
@@ -122,7 +124,7 @@ impl DatabentoHistoricalClient {
.limit(limit.and_then(NonZeroU64::new))
.build();
- let publishers = self.publishers.clone();
+ let publisher_venue_map = self.publisher_venue_map.clone();
let ts_init = self.clock.get_time_ns();
pyo3_asyncio::tokio::future_into_py(py, async move {
@@ -138,9 +140,12 @@ impl DatabentoHistoricalClient {
let mut instruments = Vec::new();
while let Ok(Some(rec)) = decoder.decode_record::().await {
- let publisher_id = rec.publisher().unwrap() as PublisherId;
- let publisher = publishers.get(&publisher_id).unwrap();
- let result = parse_instrument_def_msg(rec, publisher, ts_init);
+ let raw_symbol = unsafe { raw_ptr_to_ustr(rec.raw_symbol.as_ptr()).unwrap() };
+ let symbol = Symbol { value: raw_symbol };
+ let venue = publisher_venue_map.get(&rec.hd.publisher_id).unwrap();
+ let instrument_id = InstrumentId::new(symbol, *venue);
+
+ let result = decode_instrument_def_msg(rec, instrument_id, ts_init);
match result {
Ok(instrument) => instruments.push(instrument),
Err(e) => eprintln!("{e:?}"),
@@ -180,7 +185,7 @@ impl DatabentoHistoricalClient {
.build();
let price_precision = 2; // TODO: Hard coded for now
- let publishers = self.publishers.clone();
+ let publisher_venue_map = self.publisher_venue_map.clone();
let ts_init = self.clock.get_time_ns();
pyo3_asyncio::tokio::future_into_py(py, async move {
@@ -196,21 +201,21 @@ impl DatabentoHistoricalClient {
while let Ok(Some(rec)) = decoder.decode_record::().await {
let rec_ref = dbn::RecordRef::from(rec);
- let rtype = rec_ref.rtype().expect("Invalid `rtype` for data loading");
- let instrument_id = parse_nautilus_instrument_id(&rec_ref, &metadata, &publishers)
+ let venue = publisher_venue_map.get(&rec.hd.publisher_id).unwrap();
+ let instrument_id = decode_nautilus_instrument_id(&rec_ref, &metadata, *venue)
.map_err(to_pyvalue_err)?;
- let (data, _) = parse_record(
+ let (data, _) = decode_record(
&rec_ref,
- rtype,
instrument_id,
price_precision,
Some(ts_init),
+ false, // Don't include trades
)
.map_err(to_pyvalue_err)?;
match data {
- Data::Quote(quote) => {
+ Some(Data::Quote(quote)) => {
result.push(quote);
}
_ => panic!("Invalid data element not `QuoteTick`, was {data:?}"),
@@ -243,7 +248,7 @@ impl DatabentoHistoricalClient {
.build();
let price_precision = 2; // TODO: Hard coded for now
- let publishers = self.publishers.clone();
+ let publisher_venue_map = self.publisher_venue_map.clone();
let ts_init = self.clock.get_time_ns();
pyo3_asyncio::tokio::future_into_py(py, async move {
@@ -259,21 +264,21 @@ impl DatabentoHistoricalClient {
while let Ok(Some(rec)) = decoder.decode_record::().await {
let rec_ref = dbn::RecordRef::from(rec);
- let rtype = rec_ref.rtype().expect("Invalid `rtype` for data loading");
- let instrument_id = parse_nautilus_instrument_id(&rec_ref, &metadata, &publishers)
+ let venue = publisher_venue_map.get(&rec.hd.publisher_id).unwrap();
+ let instrument_id = decode_nautilus_instrument_id(&rec_ref, &metadata, *venue)
.map_err(to_pyvalue_err)?;
- let (data, _) = parse_record(
+ let (data, _) = decode_record(
&rec_ref,
- rtype,
instrument_id,
price_precision,
Some(ts_init),
+ false, // Not applicable (trade will be decoded regardless)
)
.map_err(to_pyvalue_err)?;
match data {
- Data::Trade(trade) => {
+ Some(Data::Trade(trade)) => {
result.push(trade);
}
_ => panic!("Invalid data element not `TradeTick`, was {data:?}"),
@@ -315,7 +320,7 @@ impl DatabentoHistoricalClient {
.build();
let price_precision = 2; // TODO: Hard coded for now
- let publishers = self.publishers.clone();
+ let publisher_venue_map = self.publisher_venue_map.clone();
let ts_init = self.clock.get_time_ns();
pyo3_asyncio::tokio::future_into_py(py, async move {
@@ -331,21 +336,21 @@ impl DatabentoHistoricalClient {
while let Ok(Some(rec)) = decoder.decode_record::().await {
let rec_ref = dbn::RecordRef::from(rec);
- let rtype = rec_ref.rtype().expect("Invalid `rtype` for data loading");
- let instrument_id = parse_nautilus_instrument_id(&rec_ref, &metadata, &publishers)
+ let venue = publisher_venue_map.get(&rec.hd.publisher_id).unwrap();
+ let instrument_id = decode_nautilus_instrument_id(&rec_ref, &metadata, *venue)
.map_err(to_pyvalue_err)?;
- let (data, _) = parse_record(
+ let (data, _) = decode_record(
&rec_ref,
- rtype,
instrument_id,
price_precision,
Some(ts_init),
+ false, // Not applicable
)
.map_err(to_pyvalue_err)?;
match data {
- Data::Bar(bar) => {
+ Some(Data::Bar(bar)) => {
result.push(bar);
}
_ => panic!("Invalid data element not `Bar`, was {data:?}"),
diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs
index a70ec9ad79df..5f0e76adb337 100644
--- a/nautilus_core/adapters/src/databento/python/live.rs
+++ b/nautilus_core/adapters/src/databento/python/live.rs
@@ -13,21 +13,28 @@
// limitations under the License.
// -------------------------------------------------------------------------------------------------
+use std::collections::HashMap;
+use std::ffi::CStr;
use std::fs;
use std::str::FromStr;
use std::sync::Arc;
-use anyhow::Result;
+use anyhow::{anyhow, bail, Result};
use databento::live::Subscription;
-use dbn::{PitSymbolMap, RType, Record, SymbolIndex, VersionUpgradePolicy};
+use dbn::{PitSymbolMap, Record, SymbolIndex, VersionUpgradePolicy};
use indexmap::IndexMap;
use log::{error, info};
+use nautilus_core::ffi::cvec::CVec;
use nautilus_core::python::to_pyruntime_err;
+use nautilus_core::time::AtomicTime;
use nautilus_core::{
python::to_pyvalue_err,
time::{get_atomic_clock_realtime, UnixNanos},
};
+use nautilus_model::data::delta::OrderBookDelta;
+use nautilus_model::data::deltas::OrderBookDeltas;
use nautilus_model::data::Data;
+use nautilus_model::ffi::data::deltas::orderbook_deltas_new;
use nautilus_model::identifiers::instrument_id::InstrumentId;
use nautilus_model::identifiers::symbol::Symbol;
use nautilus_model::identifiers::venue::Venue;
@@ -37,8 +44,9 @@ use pyo3::prelude::*;
use time::OffsetDateTime;
use tokio::sync::Mutex;
use tokio::time::{timeout, Duration};
+use ustr::Ustr;
-use crate::databento::parsing::{parse_instrument_def_msg, parse_record};
+use crate::databento::decode::{decode_instrument_def_msg, decode_record};
use crate::databento::types::{DatabentoPublisher, PublisherId};
use super::loader::convert_instrument_to_pyobject;
@@ -53,8 +61,7 @@ pub struct DatabentoLiveClient {
#[pyo3(get)]
pub dataset: String,
inner: Option>>,
- runtime: tokio::runtime::Runtime,
- publishers: Arc>,
+ publisher_venue_map: Arc>,
}
impl DatabentoLiveClient {
@@ -71,7 +78,8 @@ impl DatabentoLiveClient {
match &self.inner {
Some(client) => Ok(client.clone()),
None => {
- let client = self.runtime.block_on(self.initialize_client())?;
+ let rt = pyo3_asyncio::tokio::get_runtime();
+ let client = rt.block_on(self.initialize_client())?;
self.inner = Some(Arc::new(Mutex::new(client)));
Ok(self.inner.clone().unwrap())
}
@@ -86,17 +94,17 @@ impl DatabentoLiveClient {
let file_content = fs::read_to_string(publishers_path)?;
let publishers_vec: Vec =
serde_json::from_str(&file_content).map_err(to_pyvalue_err)?;
- let publishers = publishers_vec
+
+ let publisher_venue_map = publishers_vec
.into_iter()
- .map(|p| (p.publisher_id, p))
- .collect::>();
+ .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
+ .collect::>();
Ok(Self {
key,
dataset,
inner: None,
- runtime: tokio::runtime::Runtime::new()?,
- publishers: Arc::new(publishers),
+ publisher_venue_map: Arc::new(publisher_venue_map),
})
}
@@ -143,21 +151,36 @@ impl DatabentoLiveClient {
}
#[pyo3(name = "start")]
- fn py_start<'py>(&mut self, py: Python<'py>, callback: PyObject) -> PyResult<&'py PyAny> {
+ fn py_start<'py>(
+ &mut self,
+ py: Python<'py>,
+ callback: PyObject,
+ replay: bool,
+ ) -> PyResult<&'py PyAny> {
let arc_client = self.get_inner_client().map_err(to_pyruntime_err)?;
- let publishers = self.publishers.clone();
+ let publisher_venue_map = self.publisher_venue_map.clone();
+ let clock = get_atomic_clock_realtime();
+
+ let mut buffering_start = match replay {
+ true => Some(clock.get_time_ns()),
+ false => None,
+ };
pyo3_asyncio::tokio::future_into_py(py, async move {
- let clock = get_atomic_clock_realtime();
let mut client = arc_client.lock().await;
let mut symbol_map = PitSymbolMap::new();
+ let mut instrument_id_map: HashMap = HashMap::new();
let timeout_duration = Duration::from_millis(10);
let relock_interval = timeout_duration.as_nanos() as u64;
let mut lock_last_dropped_ns = 0_u64;
+ let mut buffered_deltas: HashMap> = HashMap::new();
+
client.start().await.map_err(to_pyruntime_err)?;
+ let mut deltas_count = 0_u64;
+
loop {
// Check if need to drop then re-aquire lock
let now_ns = clock.get_time_ns();
@@ -187,72 +210,87 @@ impl DatabentoLiveClient {
}
};
- let rtype = record.rtype().expect("Invalid `rtype`");
+ if let Some(msg) = record.get::() {
+ handle_error_msg(msg);
+ } else if let Some(msg) = record.get::() {
+ handle_system_msg(msg);
+ } else if let Some(msg) = record.get::() {
+ // Remove instrument ID index as the raw symbol may have changed
+ instrument_id_map.remove(&msg.hd.instrument_id);
+ handle_symbol_mapping_msg(msg, &mut symbol_map);
+ } else if let Some(msg) = record.get::() {
+ handle_instrument_def_msg(
+ msg,
+ &publisher_venue_map,
+ &mut instrument_id_map,
+ clock,
+ &callback,
+ )
+ .map_err(to_pyvalue_err)?;
+ } else {
+ let (mut data1, data2) = handle_record(
+ record,
+ &symbol_map,
+ &publisher_venue_map,
+ &mut instrument_id_map,
+ clock,
+ )
+ .map_err(to_pyvalue_err)?;
- match rtype {
- RType::SymbolMapping => {
- symbol_map.on_record(record).unwrap_or_else(|_| {
- panic!("Error updating `symbol_map` with {record:?}")
- });
- }
- RType::Error => {
- eprintln!("{record:?}"); // TODO: Just print stderr for now
- error!("{:?}", record);
- }
- RType::System => {
- println!("{record:?}"); // TODO: Just print stdout for now
- info!("{:?}", record);
- }
- RType::InstrumentDef => {
- let msg = record
- .get::()
- .expect("Error converting record to `InstrumentDefMsg`");
- let publisher_id = record.publisher().unwrap() as PublisherId;
- let publisher = publishers.get(&publisher_id).unwrap();
- let ts_init = clock.get_time_ns();
- let result = parse_instrument_def_msg(msg, publisher, ts_init);
-
- match result {
- Ok(instrument) => {
- Python::with_gil(|py| {
- let py_obj =
- convert_instrument_to_pyobject(py, instrument).unwrap();
- match callback.call1(py, (py_obj,)) {
- Ok(_) => {}
- Err(e) => eprintln!("Error on callback, {e:?}"), // Just print error for now
- };
- });
+ if let Some(msg) = record.get::() {
+ // SAFETY: An MBO message will always produce a delta
+ if let Data::Delta(delta) = data1.clone().unwrap() {
+ let buffer = buffered_deltas.entry(delta.instrument_id).or_default();
+ buffer.push(delta);
+
+ deltas_count += 1;
+ println!(
+ "Buffering delta: {} {} {:?} flags={}, buffer_len={}",
+ deltas_count,
+ delta.ts_event,
+ buffering_start,
+ msg.flags,
+ buffer.len()
+ );
+
+ // Check if last message in the packet
+ if msg.flags & dbn::flags::LAST == 0 {
+ continue; // NOT last message
}
- Err(e) => eprintln!("{e:?}"),
- }
- continue;
- }
- _ => {
- let raw_symbol = symbol_map
- .get_for_rec(&record)
- .expect("Cannot resolve raw_symbol from `symbol_map`");
- let symbol = Symbol::from_str_unchecked(raw_symbol);
- let publisher_id = record.publisher().unwrap() as PublisherId;
- let venue_str = publishers.get(&publisher_id).unwrap().venue.as_str();
- let venue = Venue::from_str_unchecked(venue_str);
+ // Check if snapshot
+ if msg.flags & dbn::flags::SNAPSHOT != 0 {
+ continue; // Buffer snapshot
+ }
- let instrument_id = InstrumentId::new(symbol, venue);
- let ts_init = clock.get_time_ns();
+ // Check if buffering a replay
+ if let Some(start_ns) = buffering_start {
+ if delta.ts_event <= start_ns {
+ continue; // Continue buffering replay
+ }
+ buffering_start = None;
+ }
- let (data, maybe_data) =
- parse_record(&record, rtype, instrument_id, 2, Some(ts_init))
- .map_err(to_pyvalue_err)?;
+ // SAFETY: We can guarantee a deltas vec exists
+ let instrument_id = delta.instrument_id;
+ let buffer = buffered_deltas.remove(&delta.instrument_id).unwrap();
+ let deltas = OrderBookDeltas::new(delta.instrument_id, buffer);
+ let deltas_cvec: CVec = deltas.deltas.into();
+ let deltas = orderbook_deltas_new(instrument_id, &deltas_cvec);
+ data1 = Some(Data::Deltas(deltas));
+ }
+ };
- Python::with_gil(|py| {
+ Python::with_gil(|py| {
+ if let Some(data) = data1 {
call_python_with_data(py, &callback, data);
+ }
- if let Some(data) = maybe_data {
- call_python_with_data(py, &callback, data);
- }
- });
- }
- }
+ if let Some(data) = data2 {
+ call_python_with_data(py, &callback, data);
+ }
+ });
+ };
}
Ok(())
})
@@ -276,6 +314,105 @@ impl DatabentoLiveClient {
}
}
+fn handle_error_msg(msg: &dbn::ErrorMsg) {
+ eprintln!("{msg:?}"); // TODO: Just print stderr for now
+ error!("{:?}", msg);
+}
+
+fn handle_system_msg(msg: &dbn::SystemMsg) {
+ println!("{msg:?}"); // TODO: Just print stdout for now
+ info!("{:?}", msg);
+}
+
+fn handle_symbol_mapping_msg(msg: &dbn::SymbolMappingMsg, symbol_map: &mut PitSymbolMap) {
+ symbol_map
+ .on_symbol_mapping(msg)
+ .unwrap_or_else(|_| panic!("Error updating `symbol_map` with {msg:?}"));
+}
+
+fn update_instrument_id_map(
+ header: &dbn::RecordHeader,
+ raw_symbol: &str,
+ publisher_venue_map: &IndexMap,
+ instrument_id_map: &mut HashMap,
+) -> InstrumentId {
+ // Check if instrument ID is already in the map
+ if let Some(&instrument_id) = instrument_id_map.get(&header.instrument_id) {
+ return instrument_id;
+ }
+
+ let symbol = Symbol {
+ value: Ustr::from(raw_symbol),
+ };
+ let venue = publisher_venue_map.get(&header.publisher_id).unwrap();
+ let instrument_id = InstrumentId::new(symbol, *venue);
+
+ instrument_id_map.insert(header.instrument_id, instrument_id);
+ instrument_id
+}
+
+fn handle_instrument_def_msg(
+ msg: &dbn::InstrumentDefMsg,
+ publisher_venue_map: &IndexMap,
+ instrument_id_map: &mut HashMap,
+ clock: &AtomicTime,
+ callback: &PyObject,
+) -> Result<()> {
+ let c_str: &CStr = unsafe { CStr::from_ptr(msg.raw_symbol.as_ptr()) };
+ let raw_symbol: &str = c_str.to_str().map_err(|e| anyhow!(e))?;
+
+ let instrument_id = update_instrument_id_map(
+ msg.header(),
+ raw_symbol,
+ publisher_venue_map,
+ instrument_id_map,
+ );
+
+ let ts_init = clock.get_time_ns();
+ let result = decode_instrument_def_msg(msg, instrument_id, ts_init);
+
+ match result {
+ Ok(instrument) => Python::with_gil(|py| {
+ let py_obj = convert_instrument_to_pyobject(py, instrument).unwrap();
+ match callback.call1(py, (py_obj,)) {
+ Ok(_) => Ok(()),
+ Err(e) => bail!(e),
+ }
+ }),
+ Err(e) => Err(e),
+ }
+}
+
+fn handle_record(
+ record: dbn::RecordRef,
+ symbol_map: &PitSymbolMap,
+ publisher_venue_map: &IndexMap,
+ instrument_id_map: &mut HashMap,
+ clock: &AtomicTime,
+) -> Result<(Option, Option)> {
+ let raw_symbol = symbol_map
+ .get_for_rec(&record)
+ .expect("Cannot resolve `raw_symbol` from `symbol_map`");
+
+ let instrument_id = update_instrument_id_map(
+ record.header(),
+ raw_symbol,
+ publisher_venue_map,
+ instrument_id_map,
+ );
+
+ let price_precision = 2; // Hard coded for now
+ let ts_init = clock.get_time_ns();
+
+ decode_record(
+ &record,
+ instrument_id,
+ price_precision,
+ Some(ts_init),
+ true, // Always include trades
+ )
+}
+
fn call_python_with_data(py: Python, callback: &PyObject, data: Data) {
let py_obj = data_to_pycapsule(py, data);
match callback.call1(py, (py_obj,)) {
diff --git a/nautilus_core/adapters/src/databento/python/loader.rs b/nautilus_core/adapters/src/databento/python/loader.rs
index d31671a703b7..49eb69f7c3f1 100644
--- a/nautilus_core/adapters/src/databento/python/loader.rs
+++ b/nautilus_core/adapters/src/databento/python/loader.rs
@@ -15,7 +15,10 @@
use std::{any::Any, collections::HashMap, path::PathBuf};
-use nautilus_core::python::to_pyvalue_err;
+use nautilus_core::{
+ ffi::cvec::CVec,
+ python::{to_pyruntime_err, to_pyvalue_err},
+};
use nautilus_model::{
data::{
bar::Bar, delta::OrderBookDelta, depth::OrderBookDepth10, quote::QuoteTick,
@@ -27,9 +30,15 @@ use nautilus_model::{
Instrument,
},
};
-use pyo3::{prelude::*, types::PyList};
+use pyo3::{
+ prelude::*,
+ types::{PyCapsule, PyList},
+};
-use crate::databento::{loader::DatabentoDataLoader, types::DatabentoPublisher};
+use crate::databento::{
+ loader::DatabentoDataLoader,
+ types::{DatabentoPublisher, PublisherId},
+};
#[pymethods]
impl DatabentoDataLoader {
@@ -38,6 +47,7 @@ impl DatabentoDataLoader {
Self::new(path.map(PathBuf::from)).map_err(to_pyvalue_err)
}
+ #[must_use]
#[pyo3(name = "get_publishers")]
pub fn py_get_publishers(&self) -> HashMap {
self.get_publishers()
@@ -46,9 +56,18 @@ impl DatabentoDataLoader {
.collect::>()
}
+ #[must_use]
#[pyo3(name = "get_dataset_for_venue")]
pub fn py_get_dataset_for_venue(&self, venue: &Venue) -> Option {
- self.get_dataset_for_venue(venue).map(|d| d.to_string())
+ self.get_dataset_for_venue(venue)
+ .map(std::string::ToString::to_string)
+ }
+
+ #[must_use]
+ #[pyo3(name = "get_venue_for_publisher")]
+ pub fn py_get_venue_for_publisher(&self, publisher_id: PublisherId) -> Option {
+ self.get_venue_for_publisher(publisher_id)
+ .map(std::string::ToString::to_string)
}
#[pyo3(name = "schema_for_file")]
@@ -86,6 +105,7 @@ impl DatabentoDataLoader {
Ok(PyList::new(py, &data).into())
}
+ /// Cannot include trades
#[pyo3(name = "load_order_book_deltas")]
pub fn py_load_order_book_deltas(
&self,
@@ -94,17 +114,18 @@ impl DatabentoDataLoader {
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, false)
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
for result in iter {
match result {
- Ok((item1, _)) => {
+ Ok((Some(item1), _)) => {
if let Data::Delta(delta) = item1 {
data.push(delta);
}
}
+ Ok((None, _)) => continue,
Err(e) => return Err(to_pyvalue_err(e)),
}
}
@@ -112,6 +133,22 @@ impl DatabentoDataLoader {
Ok(data)
}
+ #[pyo3(name = "load_order_book_deltas_as_pycapsule")]
+ pub fn py_load_order_book_deltas_as_pycapsule(
+ &self,
+ py: Python,
+ path: String,
+ instrument_id: Option,
+ include_trades: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false))
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
+
#[pyo3(name = "load_order_book_depth10")]
pub fn py_load_order_book_depth10(
&self,
@@ -120,17 +157,18 @@ impl DatabentoDataLoader {
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, false)
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
for result in iter {
match result {
- Ok((item1, _)) => {
+ Ok((Some(item1), _)) => {
if let Data::Depth10(depth) = item1 {
data.push(depth);
}
}
+ Ok((None, _)) => continue,
Err(e) => return Err(to_pyvalue_err(e)),
}
}
@@ -138,25 +176,42 @@ impl DatabentoDataLoader {
Ok(data)
}
- #[pyo3(name = "load_quote_ticks")]
- pub fn py_load_quote_ticks(
+ #[pyo3(name = "load_order_book_depth10_as_pycapsule")]
+ pub fn py_load_order_book_depth10_as_pycapsule(
&self,
+ py: Python,
path: String,
instrument_id: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, false)
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
+
+ #[pyo3(name = "load_quotes")]
+ pub fn py_load_quotes(
+ &self,
+ path: String,
+ instrument_id: Option,
+ include_trades: Option,
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false))
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
for result in iter {
match result {
- Ok((item1, _)) => {
+ Ok((Some(item1), _)) => {
if let Data::Quote(quote) = item1 {
data.push(quote);
}
}
+ Ok((None, _)) => continue,
Err(e) => return Err(to_pyvalue_err(e)),
}
}
@@ -164,15 +219,31 @@ impl DatabentoDataLoader {
Ok(data)
}
- #[pyo3(name = "load_tbbo_trade_ticks")]
- pub fn py_load_tbbo_trade_ticks(
+ #[pyo3(name = "load_quotes_as_pycapsule")]
+ pub fn py_load_quotes_as_pycapsule(
+ &self,
+ py: Python,
+ path: String,
+ instrument_id: Option,
+ include_trades: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, include_trades.unwrap_or(false))
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
+
+ #[pyo3(name = "load_tbbo_trades")]
+ pub fn py_load_tbbo_trades(
&self,
path: String,
instrument_id: Option,
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, false)
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
@@ -190,25 +261,41 @@ impl DatabentoDataLoader {
Ok(data)
}
- #[pyo3(name = "load_trade_ticks")]
- pub fn py_load_trade_ticks(
+ #[pyo3(name = "load_tbbo_trades_as_pycapsule")]
+ pub fn py_load_tbbo_trades_as_pycapsule(
+ &self,
+ py: Python,
+ path: String,
+ instrument_id: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, false)
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
+
+ #[pyo3(name = "load_trades")]
+ pub fn py_load_trades(
&self,
path: String,
instrument_id: Option,
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, false)
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
for result in iter {
match result {
- Ok((item1, _)) => {
+ Ok((Some(item1), _)) => {
if let Data::Trade(trade) = item1 {
data.push(trade);
}
}
+ Ok((None, _)) => continue,
Err(e) => return Err(to_pyvalue_err(e)),
}
}
@@ -216,6 +303,21 @@ impl DatabentoDataLoader {
Ok(data)
}
+ #[pyo3(name = "load_trades_as_pycapsule")]
+ pub fn py_load_trades_as_pycapsule(
+ &self,
+ py: Python,
+ path: String,
+ instrument_id: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, false)
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
+
#[pyo3(name = "load_bars")]
pub fn py_load_bars(
&self,
@@ -224,23 +326,39 @@ impl DatabentoDataLoader {
) -> PyResult> {
let path_buf = PathBuf::from(path);
let iter = self
- .read_records::(path_buf, instrument_id)
+ .read_records::(path_buf, instrument_id, false)
.map_err(to_pyvalue_err)?;
let mut data = Vec::new();
for result in iter {
match result {
- Ok((item1, _)) => {
+ Ok((Some(item1), _)) => {
if let Data::Bar(bar) = item1 {
data.push(bar);
}
}
+ Ok((None, _)) => continue,
Err(e) => return Err(to_pyvalue_err(e)),
}
}
Ok(data)
}
+
+ #[pyo3(name = "load_bars_as_pycapsule")]
+ pub fn py_load_bars_as_pycapsule(
+ &self,
+ py: Python,
+ path: String,
+ instrument_id: Option,
+ ) -> PyResult {
+ let path_buf = PathBuf::from(path);
+ let iter = self
+ .read_records::(path_buf, instrument_id, false)
+ .map_err(to_pyvalue_err)?;
+
+ exhaust_data_iter_to_pycapsule(py, iter)
+ }
}
pub fn convert_instrument_to_pyobject(
@@ -262,3 +380,29 @@ pub fn convert_instrument_to_pyobject(
"Unknown instrument type",
))
}
+
+fn exhaust_data_iter_to_pycapsule(
+ py: Python,
+ iter: impl Iterator- , Option)>>,
+) -> PyResult {
+ let mut data = Vec::new();
+ for result in iter {
+ match result {
+ Ok((Some(item1), None)) => data.push(item1),
+ Ok((None, Some(item2))) => data.push(item2),
+ Ok((Some(item1), Some(item2))) => {
+ data.push(item1);
+ data.push(item2);
+ }
+ Ok((None, None)) => {
+ continue;
+ }
+ Err(e) => return Err(to_pyvalue_err(e)),
+ }
+ }
+
+ let cvec: CVec = data.into();
+ let capsule = PyCapsule::new::(py, cvec, None).map_err(to_pyruntime_err)?;
+
+ Ok(capsule.into_py(py))
+}
diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs
index b24d96d0ae56..1f0ba5cded4e 100644
--- a/nautilus_core/adapters/src/databento/python/mod.rs
+++ b/nautilus_core/adapters/src/databento/python/mod.rs
@@ -13,7 +13,27 @@
// limitations under the License.
// -------------------------------------------------------------------------------------------------
+pub mod decode;
pub mod historical;
pub mod live;
pub mod loader;
-pub mod parsing;
+
+use pyo3::prelude::*;
+
+/// Loaded as nautilus_pyo3.databento
+#[pymodule]
+pub fn databento(_: Python<'_>, m: &PyModule) -> PyResult<()> {
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_equity, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_futures_contract, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_options_contract, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_mbo_msg, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_trade_msg, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_mbp1_msg, m)?)?;
+ m.add_function(wrap_pyfunction!(decode::py_decode_mbp10_msg, m)?)?;
+
+ Ok(())
+}
diff --git a/nautilus_core/adapters/src/databento/symbology.rs b/nautilus_core/adapters/src/databento/symbology.rs
index 04358cb1113b..33794dae38d8 100644
--- a/nautilus_core/adapters/src/databento/symbology.rs
+++ b/nautilus_core/adapters/src/databento/symbology.rs
@@ -16,33 +16,30 @@
use anyhow::{bail, Result};
use databento::dbn;
use dbn::Record;
-use indexmap::IndexMap;
use nautilus_model::identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue};
use ustr::Ustr;
-use super::{types::DatabentoPublisher, types::PublisherId};
-
-pub fn parse_nautilus_instrument_id(
+pub fn decode_nautilus_instrument_id(
record: &dbn::RecordRef,
metadata: &dbn::Metadata,
- publishers: &IndexMap,
+ venue: Venue,
) -> Result {
- let (publisher_id, instrument_id, nanoseconds) = match record.rtype()? {
+ let (instrument_id, nanoseconds) = match record.rtype()? {
dbn::RType::Mbo => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp0 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp1 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Mbp10 => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.ts_recv)
+ (msg.hd.instrument_id, msg.ts_recv)
}
dbn::RType::Ohlcv1S
| dbn::RType::Ohlcv1M
@@ -50,7 +47,7 @@ pub fn parse_nautilus_instrument_id(
| dbn::RType::Ohlcv1D
| dbn::RType::OhlcvEod => {
let msg = record.get::().unwrap(); // SAFETY: RType known
- (msg.hd.publisher_id, msg.hd.instrument_id, msg.hd.ts_event)
+ (msg.hd.instrument_id, msg.hd.ts_event)
}
_ => bail!("RType is currently unsupported by NautilusTrader"),
};
@@ -68,10 +65,6 @@ pub fn parse_nautilus_instrument_id(
let symbol = Symbol {
value: Ustr::from(raw_symbol),
};
- let venue_str = publishers.get(&publisher_id).unwrap().venue.as_str();
- let venue = Venue {
- value: Ustr::from(venue_str),
- };
Ok(InstrumentId::new(symbol, venue))
}
diff --git a/nautilus_core/backtest/src/engine.rs b/nautilus_core/backtest/src/engine.rs
index 94686c3540d0..aa69b45bc5d5 100644
--- a/nautilus_core/backtest/src/engine.rs
+++ b/nautilus_core/backtest/src/engine.rs
@@ -127,12 +127,9 @@ mod tests {
let mut accumulator = TimeEventAccumulator::new();
- let time_event1 =
- TimeEvent::new(Ustr::from("TEST_EVENT_1"), UUID4::new(), 100, 100).unwrap();
- let time_event2 =
- TimeEvent::new(Ustr::from("TEST_EVENT_2"), UUID4::new(), 300, 300).unwrap();
- let time_event3 =
- TimeEvent::new(Ustr::from("TEST_EVENT_3"), UUID4::new(), 200, 200).unwrap();
+ let time_event1 = TimeEvent::new(Ustr::from("TEST_EVENT_1"), UUID4::new(), 100, 100);
+ let time_event2 = TimeEvent::new(Ustr::from("TEST_EVENT_2"), UUID4::new(), 300, 300);
+ let time_event3 = TimeEvent::new(Ustr::from("TEST_EVENT_3"), UUID4::new(), 200, 200);
// Note: as_ptr returns a borrowed pointer. It is valid as long
// as the object is in scope. In this case `callback_ptr` is valid
diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml
index 6b5efda76aae..76fb5a4507c1 100644
--- a/nautilus_core/common/Cargo.toml
+++ b/nautilus_core/common/Cargo.toml
@@ -18,12 +18,14 @@ chrono = { workspace = true }
indexmap = { workspace = true }
log = { workspace = true }
pyo3 = { workspace = true, optional = true }
+pyo3-asyncio = { workspace = true, optional = true }
redis = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
ustr = { workspace = true }
rstest = { workspace = true , optional = true}
+tokio = { workspace = true }
tracing = { workspace = true }
sysinfo = "0.30.5"
# Disable default feature "tracing-log" since it interferes with custom logging
@@ -39,7 +41,7 @@ extension-module = [
"nautilus-model/extension-module",
]
ffi = ["cbindgen"]
-python = ["pyo3"]
+python = ["pyo3", "pyo3-asyncio"]
stubs = ["rstest"]
redis = ["dep:redis"]
default = ["ffi", "python", "redis"]
diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs
index eba142cb176d..2c1f1af19a27 100644
--- a/nautilus_core/common/src/clock.rs
+++ b/nautilus_core/common/src/clock.rs
@@ -23,7 +23,7 @@ use ustr::Ustr;
use crate::{
handlers::EventHandler,
- timer::{TestTimer, TimeEvent, TimeEventHandler},
+ timer::{LiveTimer, TestTimer, TimeEvent, TimeEventHandler},
};
/// Represents a type of clock.
@@ -241,9 +241,8 @@ impl Clock for TestClock {
pub struct LiveClock {
time: &'static AtomicTime,
- timers: HashMap,
+ timers: HashMap,
default_callback: Option,
- callbacks: HashMap,
}
impl LiveClock {
@@ -253,9 +252,13 @@ impl LiveClock {
time: get_atomic_clock_realtime(),
timers: HashMap::new(),
default_callback: None,
- callbacks: HashMap::new(),
}
}
+
+ #[must_use]
+ pub fn get_timers(&self) -> &HashMap {
+ &self.timers
+ }
}
impl Default for LiveClock {
@@ -304,16 +307,22 @@ impl Clock for LiveClock {
"All Python callbacks were `None`"
);
- let name_ustr = Ustr::from(name);
- match callback {
- Some(callback_py) => self.callbacks.insert(name_ustr, callback_py),
- None => None,
+ let callback = match callback {
+ Some(callback) => callback,
+ None => self.default_callback.clone().unwrap(),
};
let ts_now = self.get_time_ns();
alert_time_ns = std::cmp::max(alert_time_ns, ts_now);
- let timer = TestTimer::new(name, alert_time_ns - ts_now, ts_now, Some(alert_time_ns));
- self.timers.insert(name_ustr, timer);
+ let mut timer = LiveTimer::new(
+ name,
+ alert_time_ns - ts_now,
+ ts_now,
+ Some(alert_time_ns),
+ callback,
+ );
+ timer.start();
+ self.timers.insert(Ustr::from(name), timer);
}
fn set_timer_ns(
@@ -330,14 +339,14 @@ impl Clock for LiveClock {
"All Python callbacks were `None`"
);
- let name_ustr = Ustr::from(name);
- match callback {
- Some(callback) => self.callbacks.insert(name_ustr, callback),
- None => None,
+ let callback = match callback {
+ Some(callback) => callback,
+ None => self.default_callback.clone().unwrap(),
};
- let timer = TestTimer::new(name, interval_ns, start_time_ns, stop_time_ns);
- self.timers.insert(name_ustr, timer);
+ let mut timer = LiveTimer::new(name, interval_ns, start_time_ns, stop_time_ns, callback);
+ timer.start();
+ self.timers.insert(Ustr::from(name), timer);
}
fn next_time_ns(&self, name: &str) -> UnixNanos {
diff --git a/nautilus_core/common/src/ffi/clock.rs b/nautilus_core/common/src/ffi/clock.rs
index cea96cc17186..0cd4b3641841 100644
--- a/nautilus_core/common/src/ffi/clock.rs
+++ b/nautilus_core/common/src/ffi/clock.rs
@@ -42,8 +42,8 @@ use crate::{
/// It implements the `Deref` trait, allowing instances of `TestClock_API` to be
/// dereferenced to `TestClock`, providing access to `TestClock`'s methods without
/// having to manually access the underlying `TestClock` instance.
-#[allow(non_camel_case_types)]
#[repr(C)]
+#[allow(non_camel_case_types)]
pub struct TestClock_API(Box);
impl Deref for TestClock_API {
@@ -135,7 +135,7 @@ pub extern "C" fn test_clock_timer_count(clock: &mut TestClock_API) -> usize {
/// - Assumes `name_ptr` is a valid C string pointer.
/// - Assumes `callback_ptr` is a valid `PyCallable` pointer.
#[no_mangle]
-pub unsafe extern "C" fn test_clock_set_time_alert_ns(
+pub unsafe extern "C" fn test_clock_set_time_alert(
clock: &mut TestClock_API,
name_ptr: *const c_char,
alert_time_ns: UnixNanos,
@@ -158,7 +158,7 @@ pub unsafe extern "C" fn test_clock_set_time_alert_ns(
/// - Assumes `name_ptr` is a valid C string pointer.
/// - Assumes `callback_ptr` is a valid `PyCallable` pointer.
#[no_mangle]
-pub unsafe extern "C" fn test_clock_set_timer_ns(
+pub unsafe extern "C" fn test_clock_set_timer(
clock: &mut TestClock_API,
name_ptr: *const c_char,
interval_ns: u64,
@@ -217,7 +217,7 @@ pub extern "C" fn vec_time_event_handlers_drop(v: CVec) {
///
/// - Assumes `name_ptr` is a valid C string pointer.
#[no_mangle]
-pub unsafe extern "C" fn test_clock_next_time_ns(
+pub unsafe extern "C" fn test_clock_next_time(
clock: &mut TestClock_API,
name_ptr: *const c_char,
) -> UnixNanos {
@@ -251,8 +251,8 @@ pub extern "C" fn test_clock_cancel_timers(clock: &mut TestClock_API) {
/// dereferenced to `LiveClock`, providing access to `LiveClock`'s methods without
/// having to manually access the underlying `LiveClock` instance. This includes
/// both mutable and immutable access.
-#[allow(non_camel_case_types)]
#[repr(C)]
+#[allow(non_camel_case_types)]
pub struct LiveClock_API(Box);
impl Deref for LiveClock_API {
@@ -279,6 +279,23 @@ pub extern "C" fn live_clock_drop(clock: LiveClock_API) {
drop(clock); // Memory freed here
}
+/// # Safety
+///
+/// - Assumes `callback_ptr` is a valid `PyCallable` pointer.
+#[no_mangle]
+pub unsafe extern "C" fn live_clock_register_default_handler(
+ clock: &mut LiveClock_API,
+ callback_ptr: *mut ffi::PyObject,
+) {
+ assert!(!callback_ptr.is_null());
+ assert!(ffi::Py_None() != callback_ptr);
+
+ let callback_py = Python::with_gil(|py| PyObject::from_borrowed_ptr(py, callback_ptr));
+ let handler = EventHandler::new(Some(callback_py), None);
+
+ clock.register_default_handler(handler);
+}
+
#[no_mangle]
pub extern "C" fn live_clock_timestamp(clock: &mut LiveClock_API) -> f64 {
clock.get_time()
@@ -298,3 +315,109 @@ pub extern "C" fn live_clock_timestamp_us(clock: &mut LiveClock_API) -> u64 {
pub extern "C" fn live_clock_timestamp_ns(clock: &mut LiveClock_API) -> u64 {
clock.get_time_ns()
}
+
+#[no_mangle]
+pub extern "C" fn live_clock_timer_names(clock: &LiveClock_API) -> *mut ffi::PyObject {
+ Python::with_gil(|py| -> Py {
+ let names: Vec> = clock
+ .get_timers()
+ .keys()
+ .map(|k| PyString::new(py, k).into())
+ .collect();
+ PyList::new(py, names).into()
+ })
+ .as_ptr()
+}
+
+#[no_mangle]
+pub extern "C" fn live_clock_timer_count(clock: &mut LiveClock_API) -> usize {
+ clock.timer_count()
+}
+
+/// # Safety
+///
+/// - Assumes `name_ptr` is a valid C string pointer.
+/// - Assumes `callback_ptr` is a valid `PyCallable` pointer.
+#[no_mangle]
+pub unsafe extern "C" fn live_clock_set_time_alert(
+ clock: &mut LiveClock_API,
+ name_ptr: *const c_char,
+ alert_time_ns: UnixNanos,
+ callback_ptr: *mut ffi::PyObject,
+) {
+ assert!(!callback_ptr.is_null());
+
+ let name = cstr_to_str(name_ptr);
+ let callback_py = Python::with_gil(|py| match callback_ptr {
+ ptr if ptr != ffi::Py_None() => Some(PyObject::from_borrowed_ptr(py, ptr)),
+ _ => None,
+ });
+ let handler = EventHandler::new(callback_py.clone(), None);
+
+ clock.set_time_alert_ns(name, alert_time_ns, callback_py.map(|_| handler));
+}
+
+/// # Safety
+///
+/// - Assumes `name_ptr` is a valid C string pointer.
+/// - Assumes `callback_ptr` is a valid `PyCallable` pointer.
+#[no_mangle]
+pub unsafe extern "C" fn live_clock_set_timer(
+ clock: &mut LiveClock_API,
+ name_ptr: *const c_char,
+ interval_ns: u64,
+ start_time_ns: UnixNanos,
+ stop_time_ns: UnixNanos,
+ callback_ptr: *mut ffi::PyObject,
+) {
+ assert!(!callback_ptr.is_null());
+
+ let name = cstr_to_str(name_ptr);
+ let stop_time_ns = match stop_time_ns {
+ 0 => None,
+ _ => Some(stop_time_ns),
+ };
+ let callback_py = Python::with_gil(|py| match callback_ptr {
+ ptr if ptr != ffi::Py_None() => Some(PyObject::from_borrowed_ptr(py, ptr)),
+ _ => None,
+ });
+
+ let handler = EventHandler::new(callback_py.clone(), None);
+
+ clock.set_timer_ns(
+ name,
+ interval_ns,
+ start_time_ns,
+ stop_time_ns,
+ callback_py.map(|_| handler),
+ );
+}
+
+/// # Safety
+///
+/// - Assumes `name_ptr` is a valid C string pointer.
+#[no_mangle]
+pub unsafe extern "C" fn live_clock_next_time(
+ clock: &mut LiveClock_API,
+ name_ptr: *const c_char,
+) -> UnixNanos {
+ let name = cstr_to_str(name_ptr);
+ clock.next_time_ns(name)
+}
+
+/// # Safety
+///
+/// - Assumes `name_ptr` is a valid C string pointer.
+#[no_mangle]
+pub unsafe extern "C" fn live_clock_cancel_timer(
+ clock: &mut LiveClock_API,
+ name_ptr: *const c_char,
+) {
+ let name = cstr_to_str(name_ptr);
+ clock.cancel_timer(name);
+}
+
+#[no_mangle]
+pub extern "C" fn live_clock_cancel_timers(clock: &mut LiveClock_API) {
+ clock.cancel_timers();
+}
diff --git a/nautilus_core/common/src/ffi/msgbus.rs b/nautilus_core/common/src/ffi/msgbus.rs
index aad12a2d13d3..1c944bc7f325 100644
--- a/nautilus_core/common/src/ffi/msgbus.rs
+++ b/nautilus_core/common/src/ffi/msgbus.rs
@@ -48,8 +48,8 @@ use crate::{
/// It implements the `Deref` trait, allowing instances of `MessageBus_API` to be
/// dereferenced to `MessageBus`, providing access to `TestClock`'s methods without
/// having to manually access the underlying `MessageBus` instance.
-#[allow(non_camel_case_types)]
#[repr(C)]
+#[allow(non_camel_case_types)]
pub struct MessageBus_API(Box);
impl Deref for MessageBus_API {
diff --git a/nautilus_core/common/src/ffi/timer.rs b/nautilus_core/common/src/ffi/timer.rs
index 9976cd9609ef..66ad4a6b9558 100644
--- a/nautilus_core/common/src/ffi/timer.rs
+++ b/nautilus_core/common/src/ffi/timer.rs
@@ -32,7 +32,7 @@ pub unsafe extern "C" fn time_event_new(
ts_event: u64,
ts_init: u64,
) -> TimeEvent {
- TimeEvent::new(cstr_to_ustr(name_ptr), event_id, ts_event, ts_init).unwrap()
+ TimeEvent::new(cstr_to_ustr(name_ptr), event_id, ts_event, ts_init)
}
/// Returns a [`TimeEvent`] as a C string pointer.
diff --git a/nautilus_core/common/src/handlers.rs b/nautilus_core/common/src/handlers.rs
index 8771b6442ae4..bed9d5f06965 100644
--- a/nautilus_core/common/src/handlers.rs
+++ b/nautilus_core/common/src/handlers.rs
@@ -94,12 +94,11 @@ impl fmt::Debug for MessageHandler {
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common")
)]
pub struct EventHandler {
- py_callback: Option,
+ pub py_callback: Option,
_callback: Option,
}
impl EventHandler {
- // TODO: Validate exactly one of these is `Some`
#[must_use]
pub fn new(py_callback: Option, callback: Option) -> Self {
Self {
diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs
index 5f751bc1d999..2c8b5d4b639f 100644
--- a/nautilus_core/common/src/logging/mod.rs
+++ b/nautilus_core/common/src/logging/mod.rs
@@ -272,10 +272,10 @@ pub fn init_logging(
/// channel.
#[derive(Debug)]
pub struct Logger {
- /// Send log events to a different thread.
- tx: Sender,
/// Configure maximum levels for components and IO.
pub config: LoggerConfig,
+ /// Send log events to a different thread.
+ tx: Sender,
}
/// Represents a type of log event.
@@ -361,8 +361,9 @@ impl fmt::Display for LogLine {
impl Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
!LOGGING_BYPASSED.load(Ordering::Relaxed)
- && (metadata.level() >= self.config.stdout_level
- || metadata.level() >= self.config.fileout_level)
+ && (metadata.level() == Level::Error
+ || metadata.level() <= self.config.stdout_level
+ || metadata.level() <= self.config.fileout_level)
}
fn log(&self, record: &log::Record) {
@@ -417,7 +418,7 @@ impl Logger {
let print_config = config.print_config;
if print_config {
println!("STATIC_MAX_LEVEL={STATIC_MAX_LEVEL}");
- println!("Logger initialized with {:?}", config);
+ println!("Logger initialized with {:?} {:?}", config, file_config);
}
match set_boxed_logger(Box::new(logger)) {
diff --git a/nautilus_core/common/src/logging/writer.rs b/nautilus_core/common/src/logging/writer.rs
index 865af38cb27c..c9f555d70b8c 100644
--- a/nautilus_core/common/src/logging/writer.rs
+++ b/nautilus_core/common/src/logging/writer.rs
@@ -35,9 +35,9 @@ pub trait LogWriter {
#[derive(Debug)]
pub struct StdoutWriter {
+ pub is_colored: bool,
buf: BufWriter,
level: LevelFilter,
- pub is_colored: bool,
}
impl StdoutWriter {
@@ -72,8 +72,8 @@ impl LogWriter for StdoutWriter {
#[derive(Debug)]
pub struct StderrWriter {
- buf: BufWriter,
pub is_colored: bool,
+ buf: BufWriter,
}
impl StderrWriter {
@@ -132,12 +132,12 @@ impl FileWriterConfig {
#[derive(Debug)]
pub struct FileWriter {
+ pub json_format: bool,
buf: BufWriter,
path: PathBuf,
file_config: FileWriterConfig,
trader_id: String,
instance_id: String,
- pub json_format: bool,
level: LevelFilter,
}
@@ -169,12 +169,12 @@ impl FileWriter {
.open(file_path.clone())
{
Ok(file) => Some(Self {
+ json_format,
buf: BufWriter::new(file),
path: file_path,
file_config,
trader_id,
instance_id,
- json_format,
level: fileout_level,
}),
Err(e) => {
diff --git a/nautilus_core/common/src/msgbus.rs b/nautilus_core/common/src/msgbus.rs
index 410513a8b351..fb206e759344 100644
--- a/nautilus_core/common/src/msgbus.rs
+++ b/nautilus_core/common/src/msgbus.rs
@@ -131,21 +131,6 @@ impl fmt::Display for BusMessage {
/// For example, `c??p` would match both of the above examples and `coop`.
#[derive(Clone)]
pub struct MessageBus {
- tx: Option>,
- /// mapping from topic to the corresponding handler
- /// a topic can be a string with wildcards
- /// * '?' - any character
- /// * '*' - any number of any characters
- subscriptions: IndexMap>,
- /// maps a pattern to all the handlers registered for it
- /// this is updated whenever a new subscription is created.
- patterns: IndexMap>,
- /// handles a message or a request destined for a specific endpoint.
- endpoints: IndexMap,
- /// Relates a request with a response
- /// a request maps it's id to a handler so that a response
- /// with the same id can later be handled.
- correlation_index: IndexMap,
/// The trader ID associated with the message bus.
pub trader_id: TraderId,
/// The instance ID associated with the message bus.
@@ -162,6 +147,21 @@ pub struct MessageBus {
pub pub_count: u64,
/// If the message bus is backed by a database.
pub has_backing: bool,
+ tx: Option>,
+ /// mapping from topic to the corresponding handler
+ /// a topic can be a string with wildcards
+ /// * '?' - any character
+ /// * '*' - any number of any characters
+ subscriptions: IndexMap>,
+ /// maps a pattern to all the handlers registered for it
+ /// this is updated whenever a new subscription is created.
+ patterns: IndexMap>,
+ /// handles a message or a request destined for a specific endpoint.
+ endpoints: IndexMap,
+ /// Relates a request with a response
+ /// a request maps it's id to a handler so that a response
+ /// with the same id can later be handled.
+ correlation_index: IndexMap,
}
impl MessageBus {
diff --git a/nautilus_core/common/src/python/timer.rs b/nautilus_core/common/src/python/timer.rs
index 735c33ca68d2..534f17dd4d16 100644
--- a/nautilus_core/common/src/python/timer.rs
+++ b/nautilus_core/common/src/python/timer.rs
@@ -29,13 +29,8 @@ use crate::timer::TimeEvent;
#[pymethods]
impl TimeEvent {
#[new]
- fn py_new(
- name: &str,
- event_id: UUID4,
- ts_event: UnixNanos,
- ts_init: UnixNanos,
- ) -> PyResult {
- Self::new(Ustr::from(name), event_id, ts_event, ts_init).map_err(to_pyvalue_err)
+ fn py_new(name: &str, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
+ Self::new(Ustr::from(name), event_id, ts_event, ts_init)
}
fn __setstate__(&mut self, py: Python, state: PyObject) -> PyResult<()> {
@@ -66,8 +61,8 @@ impl TimeEvent {
}
#[staticmethod]
- fn _safe_constructor() -> PyResult {
- Ok(Self::new(Ustr::from("NULL"), UUID4::new(), 0, 0).unwrap()) // Safe default
+ fn _safe_constructor() -> Self {
+ Self::new(Ustr::from("NULL"), UUID4::new(), 0, 0)
}
fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py {
diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs
index 18bf5f42f3b6..a1127ed8bf78 100644
--- a/nautilus_core/common/src/timer.rs
+++ b/nautilus_core/common/src/timer.rs
@@ -16,17 +16,20 @@
use std::{
cmp::Ordering,
fmt::{Display, Formatter},
+ time::Duration,
};
-use anyhow::Result;
use nautilus_core::{
correctness::check_valid_string,
- time::{TimedeltaNanos, UnixNanos},
+ time::{get_atomic_clock_realtime, TimedeltaNanos, UnixNanos},
uuid::UUID4,
};
-use pyo3::ffi;
+use pyo3::{ffi, types::PyCapsule, IntoPy, PyObject, Python};
+use tokio::sync::oneshot;
use ustr::Ustr;
+use crate::handlers::EventHandler;
+
#[repr(C)]
#[derive(Clone, Debug)]
#[allow(clippy::redundant_allocation)] // C ABI compatibility
@@ -46,19 +49,15 @@ pub struct TimeEvent {
pub ts_init: UnixNanos,
}
+/// Assumes `name` is a valid string.
impl TimeEvent {
- pub fn new(
- name: Ustr,
- event_id: UUID4,
- ts_event: UnixNanos,
- ts_init: UnixNanos,
- ) -> Result {
- Ok(Self {
+ pub fn new(name: Ustr, event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
+ Self {
name,
event_id,
ts_event,
ts_init,
- })
+ }
}
}
@@ -120,7 +119,8 @@ pub trait Timer {
fn cancel(&mut self);
}
-#[derive(Clone, Copy)]
+/// Provides a test timer for user with a [`TestClock`].
+#[derive(Clone, Copy, Debug)]
pub struct TestTimer {
pub name: Ustr,
pub interval_ns: u64,
@@ -153,7 +153,7 @@ impl TestTimer {
#[must_use]
pub fn pop_event(&self, event_id: UUID4, ts_init: UnixNanos) -> TimeEvent {
TimeEvent {
- name: Ustr::from(&self.name),
+ name: self.name,
event_id,
ts_event: self.next_time_ns,
ts_init,
@@ -206,6 +206,117 @@ impl Iterator for TestTimer {
}
}
+/// Provides a live timer for use with a [`LiveClock`].
+///
+/// Note: `next_time_ns` is only accurate when initially starting the timer
+/// and will not incrementally update as the timer runs.
+pub struct LiveTimer {
+ pub name: Ustr,
+ pub interval_ns: u64,
+ pub start_time_ns: UnixNanos,
+ pub stop_time_ns: Option,
+ pub next_time_ns: UnixNanos,
+ pub is_expired: bool,
+ callback: EventHandler,
+ canceler: Option>,
+}
+
+impl LiveTimer {
+ #[must_use]
+ pub fn new(
+ name: &str,
+ interval_ns: u64,
+ start_time_ns: UnixNanos,
+ stop_time_ns: Option,
+ callback: EventHandler,
+ ) -> Self {
+ check_valid_string(name, "`TestTimer` name").unwrap();
+
+ Self {
+ name: Ustr::from(name),
+ interval_ns,
+ start_time_ns,
+ stop_time_ns,
+ next_time_ns: start_time_ns + interval_ns,
+ is_expired: false,
+ callback,
+ canceler: None,
+ }
+ }
+
+ pub fn start(&mut self) {
+ let event_name = self.name;
+ let mut start_time_ns = self.start_time_ns;
+ let stop_time_ns = self.stop_time_ns;
+ let interval_ns = self.interval_ns;
+
+ let callback = self
+ .callback
+ .py_callback
+ .clone()
+ .expect("No callback for event handler");
+
+ // Setup oneshot channel for cancelling timer task
+ let (cancel_tx, mut cancel_rx) = oneshot::channel();
+ self.canceler = Some(cancel_tx);
+
+ pyo3_asyncio::tokio::get_runtime().spawn(async move {
+ let clock = get_atomic_clock_realtime();
+ if start_time_ns == 0 {
+ start_time_ns = clock.get_time_ns();
+ }
+
+ let mut next_time_ns = start_time_ns + interval_ns;
+
+ loop {
+ tokio::select! {
+ _ = tokio::time::sleep(Duration::from_nanos(next_time_ns.saturating_sub(clock.get_time_ns()))) => {
+ Python::with_gil(|py| {
+ // Create new time event
+ let event = TimeEvent::new(
+ event_name,
+ UUID4::new(),
+ next_time_ns,
+ clock.get_time_ns()
+ );
+ let capsule: PyObject = PyCapsule::new(py, event, None).expect("Error creating `PyCapsule`").into_py(py);
+
+ match callback.call1(py, (capsule,)) {
+ Ok(_) => {},
+ Err(e) => eprintln!("Error on callback: {:?}", e),
+ };
+ });
+
+ // Prepare next time interval
+ next_time_ns += interval_ns;
+
+ // Check if expired
+ if let Some(stop_time_ns) = stop_time_ns {
+ if next_time_ns >= stop_time_ns {
+ break; // Timer expired
+ }
+ }
+ },
+ _ = (&mut cancel_rx) => {
+ break; // Timer canceled
+ },
+ }
+ }
+
+ Ok::<(), anyhow::Error>(())
+ });
+
+ self.is_expired = true;
+ }
+
+ /// Cancels the timer (the timer will not generate an event).
+ pub fn cancel(&mut self) {
+ if let Some(sender) = self.canceler.take() {
+ let _ = sender.send(());
+ }
+ }
+}
+
////////////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////////////
@@ -216,7 +327,7 @@ mod tests {
use super::{TestTimer, TimeEvent};
#[rstest]
- fn test_pop_event() {
+ fn test_test_timer_pop_event() {
let mut timer = TestTimer::new("test_timer", 0, 1, None);
assert!(timer.next().is_some());
@@ -226,7 +337,7 @@ mod tests {
}
#[rstest]
- fn test_advance_within_next_time_ns() {
+ fn test_test_timer_advance_within_next_time_ns() {
let mut timer = TestTimer::new("test_timer", 5, 0, None);
let _: Vec = timer.advance(1).collect();
let _: Vec = timer.advance(2).collect();
@@ -237,28 +348,28 @@ mod tests {
}
#[rstest]
- fn test_advance_up_to_next_time_ns() {
+ fn test_test_timer_advance_up_to_next_time_ns() {
let mut timer = TestTimer::new("test_timer", 1, 0, None);
assert_eq!(timer.advance(1).count(), 1);
assert!(!timer.is_expired);
}
#[rstest]
- fn test_advance_up_to_next_time_ns_with_stop_time() {
+ fn test_test_timer_advance_up_to_next_time_ns_with_stop_time() {
let mut timer = TestTimer::new("test_timer", 1, 0, Some(2));
assert_eq!(timer.advance(2).count(), 2);
assert!(timer.is_expired);
}
#[rstest]
- fn test_advance_beyond_next_time_ns() {
+ fn test_test_timer_advance_beyond_next_time_ns() {
let mut timer = TestTimer::new("test_timer", 1, 0, Some(5));
assert_eq!(timer.advance(5).count(), 5);
assert!(timer.is_expired);
}
#[rstest]
- fn test_advance_beyond_stop_time() {
+ fn test_test_timer_advance_beyond_stop_time() {
let mut timer = TestTimer::new("test_timer", 1, 0, Some(5));
assert_eq!(timer.advance(10).count(), 5);
assert!(timer.is_expired);
diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs
index 7adab03de47b..088e3646de5f 100644
--- a/nautilus_core/core/src/datetime.rs
+++ b/nautilus_core/core/src/datetime.rs
@@ -18,7 +18,7 @@ use std::time::{Duration, UNIX_EPOCH};
use anyhow::{anyhow, Result};
use chrono::{
prelude::{DateTime, Utc},
- Datelike, NaiveDate, SecondsFormat, Weekday,
+ Datelike, NaiveDate, SecondsFormat, TimeDelta, Weekday,
};
use crate::time::UnixNanos;
@@ -105,7 +105,7 @@ pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> Result
});
// Calculate last closest weekday
- let last_closest = date - chrono::Duration::days(offset);
+ let last_closest = date - TimeDelta::days(offset);
// Convert to UNIX nanoseconds
let unix_timestamp_ns = last_closest
@@ -124,7 +124,7 @@ pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> Result {
.ok_or_else(|| anyhow!("Invalid timestamp {timestamp_ns}"))?;
let now = Utc::now();
- Ok(now.signed_duration_since(timestamp) <= chrono::Duration::days(1))
+ Ok(now.signed_duration_since(timestamp) <= TimeDelta::days(1))
}
////////////////////////////////////////////////////////////////////////////////
@@ -261,7 +261,7 @@ mod tests {
#[rstest]
fn test_is_within_last_24_hours_when_two_days_ago() {
- let past_ns = (Utc::now() - chrono::Duration::days(2))
+ let past_ns = (Utc::now() - TimeDelta::days(2))
.timestamp_nanos_opt()
.unwrap();
assert!(!is_within_last_24_hours(past_ns as UnixNanos).unwrap());
diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs
index 12783908f129..6c27aa312804 100644
--- a/nautilus_core/core/src/uuid.rs
+++ b/nautilus_core/core/src/uuid.rs
@@ -32,7 +32,8 @@ use uuid::Uuid;
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core")
)]
pub struct UUID4 {
- pub value: [u8; 37],
+ /// The UUID v4 C string value as a fixed-length byte array.
+ pub(crate) value: [u8; 37],
}
impl UUID4 {
diff --git a/nautilus_core/indicators/src/average/ama.rs b/nautilus_core/indicators/src/average/ama.rs
index d84fff681634..5271cc07dbd9 100644
--- a/nautilus_core/indicators/src/average/ama.rs
+++ b/nautilus_core/indicators/src/average/ama.rs
@@ -48,12 +48,12 @@ pub struct AdaptiveMovingAverage {
pub value: f64,
/// The input count for the indicator.
pub count: usize,
- pub is_initialized: bool,
- _efficiency_ratio: EfficiencyRatio,
- _prior_value: Option,
- _alpha_fast: f64,
- _alpha_slow: f64,
+ pub initialized: bool,
has_inputs: bool,
+ efficiency_ratio: EfficiencyRatio,
+ prior_value: Option,
+ alpha_fast: f64,
+ alpha_slow: f64,
}
impl Display for AdaptiveMovingAverage {
@@ -78,8 +78,8 @@ impl Indicator for AdaptiveMovingAverage {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
fn handle_quote_tick(&mut self, tick: &QuoteTick) {
@@ -98,7 +98,7 @@ impl Indicator for AdaptiveMovingAverage {
self.value = 0.0;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -118,26 +118,26 @@ impl AdaptiveMovingAverage {
price_type: price_type.unwrap_or(PriceType::Last),
value: 0.0,
count: 0,
- _alpha_fast: 2.0 / (period_fast + 1) as f64,
- _alpha_slow: 2.0 / (period_slow + 1) as f64,
- _prior_value: None,
+ alpha_fast: 2.0 / (period_fast + 1) as f64,
+ alpha_slow: 2.0 / (period_slow + 1) as f64,
+ prior_value: None,
has_inputs: false,
- is_initialized: false,
- _efficiency_ratio: EfficiencyRatio::new(period_efficiency_ratio, price_type)?,
+ initialized: false,
+ efficiency_ratio: EfficiencyRatio::new(period_efficiency_ratio, price_type)?,
})
}
#[must_use]
pub fn alpha_diff(&self) -> f64 {
- self._alpha_fast - self._alpha_slow
+ self.alpha_fast - self.alpha_slow
}
pub fn reset(&mut self) {
self.value = 0.0;
- self._prior_value = None;
+ self.prior_value = None;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -152,29 +152,27 @@ impl MovingAverage for AdaptiveMovingAverage {
fn update_raw(&mut self, value: f64) {
if !self.has_inputs {
- self._prior_value = Some(value);
- self._efficiency_ratio.update_raw(value);
+ self.prior_value = Some(value);
+ self.efficiency_ratio.update_raw(value);
self.value = value;
self.has_inputs = true;
return;
}
- self._efficiency_ratio.update_raw(value);
- self._prior_value = Some(self.value);
+ self.efficiency_ratio.update_raw(value);
+ self.prior_value = Some(self.value);
// Calculate the smoothing constant
let smoothing_constant = self
- ._efficiency_ratio
+ .efficiency_ratio
.value
- .mul_add(self.alpha_diff(), self._alpha_slow)
+ .mul_add(self.alpha_diff(), self.alpha_slow)
.powi(2);
// Calculate the AMA
- self.value = smoothing_constant.mul_add(
- value - self._prior_value.unwrap(),
- self._prior_value.unwrap(),
- );
- if self._efficiency_ratio.is_initialized() {
- self.is_initialized = true;
+ self.value = smoothing_constant
+ .mul_add(value - self.prior_value.unwrap(), self.prior_value.unwrap());
+ if self.efficiency_ratio.initialized() {
+ self.initialized = true;
}
}
}
@@ -199,7 +197,7 @@ mod tests {
assert_eq!(display_str, "AdaptiveMovingAverage(10,2,30)");
assert_eq!(indicator_ama_10.name(), "AdaptiveMovingAverage");
assert!(!indicator_ama_10.has_inputs());
- assert!(!indicator_ama_10.is_initialized());
+ assert!(!indicator_ama_10.initialized());
}
#[rstest]
@@ -228,9 +226,9 @@ mod tests {
for _ in 0..10 {
indicator_ama_10.update_raw(1.0);
}
- assert!(indicator_ama_10.is_initialized);
+ assert!(indicator_ama_10.initialized);
indicator_ama_10.reset();
- assert!(!indicator_ama_10.is_initialized);
+ assert!(!indicator_ama_10.initialized);
assert!(!indicator_ama_10.has_inputs);
assert_eq!(indicator_ama_10.value, 0.0);
}
@@ -241,16 +239,16 @@ mod tests {
for _ in 0..9 {
ama.update_raw(1.0);
}
- assert!(!ama.is_initialized);
+ assert!(!ama.initialized);
ama.update_raw(1.0);
- assert!(ama.is_initialized);
+ assert!(ama.initialized);
}
#[rstest]
fn test_handle_quote_tick(mut indicator_ama_10: AdaptiveMovingAverage, quote_tick: QuoteTick) {
indicator_ama_10.handle_quote_tick("e_tick);
assert!(indicator_ama_10.has_inputs);
- assert!(!indicator_ama_10.is_initialized);
+ assert!(!indicator_ama_10.initialized);
assert_eq!(indicator_ama_10.value, 1501.0);
}
@@ -261,7 +259,7 @@ mod tests {
) {
indicator_ama_10.handle_trade_tick(&trade_tick);
assert!(indicator_ama_10.has_inputs);
- assert!(!indicator_ama_10.is_initialized);
+ assert!(!indicator_ama_10.initialized);
assert_eq!(indicator_ama_10.value, 1500.0);
}
@@ -272,7 +270,7 @@ mod tests {
) {
indicator_ama_10.handle_bar(&bar_ethusdt_binance_minute_bid);
assert!(indicator_ama_10.has_inputs);
- assert!(!indicator_ama_10.is_initialized);
+ assert!(!indicator_ama_10.initialized);
assert_eq!(indicator_ama_10.value, 1522.0);
}
}
diff --git a/nautilus_core/indicators/src/average/dema.rs b/nautilus_core/indicators/src/average/dema.rs
index 6b86dd213fa2..0da1c05058c6 100644
--- a/nautilus_core/indicators/src/average/dema.rs
+++ b/nautilus_core/indicators/src/average/dema.rs
@@ -41,10 +41,10 @@ pub struct DoubleExponentialMovingAverage {
pub value: f64,
/// The input count for the indicator.
pub count: usize,
- pub is_initialized: bool,
+ pub initialized: bool,
has_inputs: bool,
- _ema1: ExponentialMovingAverage,
- _ema2: ExponentialMovingAverage,
+ ema1: ExponentialMovingAverage,
+ ema2: ExponentialMovingAverage,
}
impl Display for DoubleExponentialMovingAverage {
@@ -61,16 +61,16 @@ impl Indicator for DoubleExponentialMovingAverage {
fn has_inputs(&self) -> bool {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -81,7 +81,7 @@ impl Indicator for DoubleExponentialMovingAverage {
self.value = 0.0;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -93,9 +93,9 @@ impl DoubleExponentialMovingAverage {
value: 0.0,
count: 0,
has_inputs: false,
- is_initialized: false,
- _ema1: ExponentialMovingAverage::new(period, price_type)?,
- _ema2: ExponentialMovingAverage::new(period, price_type)?,
+ initialized: false,
+ ema1: ExponentialMovingAverage::new(period, price_type)?,
+ ema2: ExponentialMovingAverage::new(period, price_type)?,
})
}
}
@@ -113,14 +113,14 @@ impl MovingAverage for DoubleExponentialMovingAverage {
self.has_inputs = true;
self.value = value;
}
- self._ema1.update_raw(value);
- self._ema2.update_raw(self._ema1.value);
+ self.ema1.update_raw(value);
+ self.ema2.update_raw(self.ema1.value);
- self.value = 2.0f64.mul_add(self._ema1.value, -self._ema2.value);
+ self.value = 2.0f64.mul_add(self.ema1.value, -self.ema2.value);
self.count += 1;
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -144,7 +144,7 @@ mod tests {
let display_str = format!("{indicator_dema_10}");
assert_eq!(display_str, "DoubleExponentialMovingAverage(period=10)");
assert_eq!(indicator_dema_10.period, 10);
- assert!(!indicator_dema_10.is_initialized);
+ assert!(!indicator_dema_10.initialized);
assert!(!indicator_dema_10.has_inputs);
}
@@ -167,9 +167,9 @@ mod tests {
for i in 1..10 {
indicator_dema_10.update_raw(f64::from(i));
}
- assert!(!indicator_dema_10.is_initialized);
+ assert!(!indicator_dema_10.initialized);
indicator_dema_10.update_raw(10.0);
- assert!(indicator_dema_10.is_initialized);
+ assert!(indicator_dema_10.initialized);
}
#[rstest]
@@ -198,7 +198,7 @@ mod tests {
indicator_dema_10.handle_bar(&bar_ethusdt_binance_minute_bid);
assert_eq!(indicator_dema_10.value, 1522.0);
assert!(indicator_dema_10.has_inputs);
- assert!(!indicator_dema_10.is_initialized);
+ assert!(!indicator_dema_10.initialized);
}
#[rstest]
@@ -209,6 +209,6 @@ mod tests {
assert_eq!(indicator_dema_10.value, 0.0);
assert_eq!(indicator_dema_10.count, 0);
assert!(!indicator_dema_10.has_inputs);
- assert!(!indicator_dema_10.is_initialized);
+ assert!(!indicator_dema_10.initialized);
}
}
diff --git a/nautilus_core/indicators/src/average/ema.rs b/nautilus_core/indicators/src/average/ema.rs
index 4c07fb5b5529..1abca25504c4 100644
--- a/nautilus_core/indicators/src/average/ema.rs
+++ b/nautilus_core/indicators/src/average/ema.rs
@@ -33,7 +33,7 @@ pub struct ExponentialMovingAverage {
pub alpha: f64,
pub value: f64,
pub count: usize,
- pub is_initialized: bool,
+ pub initialized: bool,
has_inputs: bool,
}
@@ -52,16 +52,16 @@ impl Indicator for ExponentialMovingAverage {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -72,7 +72,7 @@ impl Indicator for ExponentialMovingAverage {
self.value = 0.0;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -87,7 +87,7 @@ impl ExponentialMovingAverage {
value: 0.0,
count: 0,
has_inputs: false,
- is_initialized: false,
+ initialized: false,
})
}
}
@@ -110,8 +110,8 @@ impl MovingAverage for ExponentialMovingAverage {
self.count += 1;
// Initialization logic
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -141,7 +141,7 @@ mod tests {
assert_eq!(ema.period, 10);
assert_eq!(ema.price_type, PriceType::Mid);
assert_eq!(ema.alpha, 0.181_818_181_818_181_82);
- assert!(!ema.is_initialized);
+ assert!(!ema.initialized);
}
#[rstest]
@@ -167,7 +167,7 @@ mod tests {
ema.update_raw(10.0);
assert!(ema.has_inputs());
- assert!(ema.is_initialized());
+ assert!(ema.initialized());
assert_eq!(ema.count, 10);
assert_eq!(ema.value, 6.239_368_480_121_215_5);
}
@@ -180,7 +180,7 @@ mod tests {
ema.reset();
assert_eq!(ema.count, 0);
assert_eq!(ema.value, 0.0);
- assert!(!ema.is_initialized);
+ assert!(!ema.initialized);
}
#[rstest]
@@ -220,7 +220,7 @@ mod tests {
) {
indicator_ema_10.handle_bar(&bar_ethusdt_binance_minute_bid);
assert!(indicator_ema_10.has_inputs);
- assert!(!indicator_ema_10.is_initialized);
+ assert!(!indicator_ema_10.initialized);
assert_eq!(indicator_ema_10.value, 1522.0);
}
}
diff --git a/nautilus_core/indicators/src/average/hma.rs b/nautilus_core/indicators/src/average/hma.rs
index a4ff4672592d..2eae32214c17 100644
--- a/nautilus_core/indicators/src/average/hma.rs
+++ b/nautilus_core/indicators/src/average/hma.rs
@@ -38,11 +38,11 @@ pub struct HullMovingAverage {
pub price_type: PriceType,
pub value: f64,
pub count: usize,
- pub is_initialized: bool,
+ pub initialized: bool,
has_inputs: bool,
- _ma1: WeightedMovingAverage,
- _ma2: WeightedMovingAverage,
- _ma3: WeightedMovingAverage,
+ ma1: WeightedMovingAverage,
+ ma2: WeightedMovingAverage,
+ ma3: WeightedMovingAverage,
}
impl Display for HullMovingAverage {
@@ -60,16 +60,16 @@ impl Indicator for HullMovingAverage {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -78,12 +78,12 @@ impl Indicator for HullMovingAverage {
fn reset(&mut self) {
self.value = 0.0;
- self._ma1.reset();
- self._ma2.reset();
- self._ma3.reset();
+ self.ma1.reset();
+ self.ma2.reset();
+ self.ma3.reset();
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -113,10 +113,10 @@ impl HullMovingAverage {
value: 0.0,
count: 0,
has_inputs: false,
- is_initialized: false,
- _ma1,
- _ma2,
- _ma3,
+ initialized: false,
+ ma1: _ma1,
+ ma2: _ma2,
+ ma3: _ma3,
})
}
}
@@ -136,16 +136,16 @@ impl MovingAverage for HullMovingAverage {
self.value = value;
}
- self._ma1.update_raw(value);
- self._ma2.update_raw(value);
- self._ma3
- .update_raw(2.0f64.mul_add(self._ma1.value, -self._ma2.value));
+ self.ma1.update_raw(value);
+ self.ma2.update_raw(value);
+ self.ma3
+ .update_raw(2.0f64.mul_add(self.ma1.value, -self.ma2.value));
- self.value = self._ma3.value;
+ self.value = self.ma3.value;
self.count += 1;
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -169,7 +169,7 @@ mod tests {
let display_str = format!("{indicator_hma_10}");
assert_eq!(display_str, "HullMovingAverage(10)");
assert_eq!(indicator_hma_10.period, 10);
- assert!(!indicator_hma_10.is_initialized);
+ assert!(!indicator_hma_10.initialized);
assert!(!indicator_hma_10.has_inputs);
}
@@ -178,9 +178,9 @@ mod tests {
for i in 1..10 {
indicator_hma_10.update_raw(f64::from(i));
}
- assert!(!indicator_hma_10.is_initialized);
+ assert!(!indicator_hma_10.initialized);
indicator_hma_10.update_raw(10.0);
- assert!(indicator_hma_10.is_initialized);
+ assert!(indicator_hma_10.initialized);
}
#[rstest]
@@ -233,7 +233,7 @@ mod tests {
indicator_hma_10.handle_bar(&bar_ethusdt_binance_minute_bid);
assert_eq!(indicator_hma_10.value, 1522.0);
assert!(indicator_hma_10.has_inputs);
- assert!(!indicator_hma_10.is_initialized);
+ assert!(!indicator_hma_10.initialized);
}
#[rstest]
@@ -241,16 +241,16 @@ mod tests {
indicator_hma_10.update_raw(1.0);
assert_eq!(indicator_hma_10.count, 1);
assert_eq!(indicator_hma_10.value, 1.0);
- assert_eq!(indicator_hma_10._ma1.value, 1.0);
- assert_eq!(indicator_hma_10._ma2.value, 1.0);
- assert_eq!(indicator_hma_10._ma3.value, 1.0);
+ assert_eq!(indicator_hma_10.ma1.value, 1.0);
+ assert_eq!(indicator_hma_10.ma2.value, 1.0);
+ assert_eq!(indicator_hma_10.ma3.value, 1.0);
indicator_hma_10.reset();
assert_eq!(indicator_hma_10.value, 0.0);
assert_eq!(indicator_hma_10.count, 0);
- assert_eq!(indicator_hma_10._ma1.value, 0.0);
- assert_eq!(indicator_hma_10._ma2.value, 0.0);
- assert_eq!(indicator_hma_10._ma3.value, 0.0);
+ assert_eq!(indicator_hma_10.ma1.value, 0.0);
+ assert_eq!(indicator_hma_10.ma2.value, 0.0);
+ assert_eq!(indicator_hma_10.ma3.value, 0.0);
assert!(!indicator_hma_10.has_inputs);
- assert!(!indicator_hma_10.is_initialized);
+ assert!(!indicator_hma_10.initialized);
}
}
diff --git a/nautilus_core/indicators/src/average/rma.rs b/nautilus_core/indicators/src/average/rma.rs
index 479e63576a81..85c3d60d01b0 100644
--- a/nautilus_core/indicators/src/average/rma.rs
+++ b/nautilus_core/indicators/src/average/rma.rs
@@ -33,7 +33,7 @@ pub struct WilderMovingAverage {
pub alpha: f64,
pub value: f64,
pub count: usize,
- pub is_initialized: bool,
+ pub initialized: bool,
has_inputs: bool,
}
@@ -52,16 +52,16 @@ impl Indicator for WilderMovingAverage {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -72,7 +72,7 @@ impl Indicator for WilderMovingAverage {
self.value = 0.0;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -90,7 +90,7 @@ impl WilderMovingAverage {
value: 0.0,
count: 0,
has_inputs: false,
- is_initialized: false,
+ initialized: false,
})
}
}
@@ -114,8 +114,8 @@ impl MovingAverage for WilderMovingAverage {
self.count += 1;
// Initialization logic
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -145,7 +145,7 @@ mod tests {
assert_eq!(rma.period, 10);
assert_eq!(rma.price_type, PriceType::Mid);
assert_eq!(rma.alpha, 0.1);
- assert!(!rma.is_initialized);
+ assert!(!rma.initialized);
}
#[rstest]
@@ -171,7 +171,7 @@ mod tests {
rma.update_raw(10.0);
assert!(rma.has_inputs());
- assert!(rma.is_initialized());
+ assert!(rma.initialized());
assert_eq!(rma.count, 10);
assert_eq!(rma.value, 4.486_784_401);
}
@@ -184,7 +184,7 @@ mod tests {
rma.reset();
assert_eq!(rma.count, 0);
assert_eq!(rma.value, 0.0);
- assert!(!rma.is_initialized);
+ assert!(!rma.initialized);
}
#[rstest]
@@ -221,7 +221,7 @@ mod tests {
) {
indicator_rma_10.handle_bar(&bar_ethusdt_binance_minute_bid);
assert!(indicator_rma_10.has_inputs);
- assert!(!indicator_rma_10.is_initialized);
+ assert!(!indicator_rma_10.initialized);
assert_eq!(indicator_rma_10.value, 1522.0);
}
}
diff --git a/nautilus_core/indicators/src/average/sma.rs b/nautilus_core/indicators/src/average/sma.rs
index 4752e1ddaaea..ae250e739250 100644
--- a/nautilus_core/indicators/src/average/sma.rs
+++ b/nautilus_core/indicators/src/average/sma.rs
@@ -33,7 +33,7 @@ pub struct SimpleMovingAverage {
pub value: f64,
pub count: usize,
pub inputs: Vec,
- pub is_initialized: bool,
+ pub initialized: bool,
}
impl Display for SimpleMovingAverage {
@@ -51,16 +51,16 @@ impl Indicator for SimpleMovingAverage {
!self.inputs.is_empty()
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -71,7 +71,7 @@ impl Indicator for SimpleMovingAverage {
self.value = 0.0;
self.count = 0;
self.inputs.clear();
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -85,7 +85,7 @@ impl SimpleMovingAverage {
value: 0.0,
count: 0,
inputs: Vec::with_capacity(period),
- is_initialized: false,
+ initialized: false,
})
}
}
@@ -108,8 +108,8 @@ impl MovingAverage for SimpleMovingAverage {
let sum = self.inputs.iter().sum::();
self.value = sum / self.count as f64;
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -156,7 +156,7 @@ mod tests {
sma.update_raw(10.0);
assert!(sma.has_inputs());
- assert!(sma.is_initialized());
+ assert!(sma.initialized());
assert_eq!(sma.count, 10);
assert_eq!(sma.value, 5.5);
}
@@ -169,7 +169,7 @@ mod tests {
sma.reset();
assert_eq!(sma.count, 0);
assert_eq!(sma.value, 0.0);
- assert!(!sma.is_initialized);
+ assert!(!sma.initialized);
}
#[rstest]
diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs
index 22ca40b08581..b0bd8a869dc6 100644
--- a/nautilus_core/indicators/src/average/wma.rs
+++ b/nautilus_core/indicators/src/average/wma.rs
@@ -38,7 +38,7 @@ pub struct WeightedMovingAverage {
/// The last indicator value.
pub value: f64,
/// Whether the indicator is initialized.
- pub is_initialized: bool,
+ pub initialized: bool,
/// Inputs
pub inputs: Vec,
has_inputs: bool,
@@ -61,7 +61,7 @@ impl WeightedMovingAverage {
price_type: price_type.unwrap_or(PriceType::Last),
value: 0.0,
inputs: Vec::with_capacity(period),
- is_initialized: false,
+ initialized: false,
has_inputs: false,
})
}
@@ -87,16 +87,16 @@ impl Indicator for WeightedMovingAverage {
fn has_inputs(&self) -> bool {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -106,7 +106,7 @@ impl Indicator for WeightedMovingAverage {
fn reset(&mut self) {
self.value = 0.0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
self.inputs.clear();
}
}
@@ -131,8 +131,8 @@ impl MovingAverage for WeightedMovingAverage {
}
self.inputs.push(value);
self.value = self.weighted_average();
- if !self.is_initialized && self.count() >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count() >= self.period {
+ self.initialized = true;
}
}
}
@@ -159,7 +159,7 @@ mod tests {
);
assert_eq!(indicator_wma_10.name(), "WeightedMovingAverage");
assert!(!indicator_wma_10.has_inputs());
- assert!(!indicator_wma_10.is_initialized());
+ assert!(!indicator_wma_10.initialized());
}
#[rstest]
@@ -233,6 +233,6 @@ mod tests {
assert_eq!(indicator_wma_10.value, 0.0);
assert_eq!(indicator_wma_10.count(), 0);
assert!(!indicator_wma_10.has_inputs);
- assert!(!indicator_wma_10.is_initialized);
+ assert!(!indicator_wma_10.initialized);
}
}
diff --git a/nautilus_core/indicators/src/book/imbalance.rs b/nautilus_core/indicators/src/book/imbalance.rs
new file mode 100644
index 000000000000..4ff0718d0917
--- /dev/null
+++ b/nautilus_core/indicators/src/book/imbalance.rs
@@ -0,0 +1,222 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use std::fmt::Display;
+
+use anyhow::Result;
+use nautilus_model::{
+ orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp},
+ types::quantity::Quantity,
+};
+use pyo3::prelude::*;
+
+use crate::indicator::Indicator;
+
+#[repr(C)]
+#[derive(Debug)]
+#[pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")]
+pub struct BookImbalanceRatio {
+ pub value: f64,
+ pub count: usize,
+ pub initialized: bool,
+ has_inputs: bool,
+}
+
+impl Display for BookImbalanceRatio {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}()", self.name())
+ }
+}
+
+impl Indicator for BookImbalanceRatio {
+ fn name(&self) -> String {
+ stringify!(BookImbalanceRatio).to_string()
+ }
+
+ fn has_inputs(&self) -> bool {
+ self.has_inputs
+ }
+
+ fn initialized(&self) -> bool {
+ self.initialized
+ }
+
+ fn handle_book_mbo(&mut self, book: &OrderBookMbo) {
+ self.update(book.best_bid_size(), book.best_ask_size())
+ }
+
+ fn handle_book_mbp(&mut self, book: &OrderBookMbp) {
+ self.update(book.best_bid_size(), book.best_ask_size())
+ }
+
+ fn reset(&mut self) {
+ self.value = 0.0;
+ self.count = 0;
+ self.has_inputs = false;
+ self.initialized = false;
+ }
+}
+
+impl BookImbalanceRatio {
+ pub fn new() -> Result {
+ // Inputs don't require validation, however we return a `Result`
+ // to standardize with other indicators which do need validation.
+ Ok(Self {
+ value: 0.0,
+ count: 0,
+ has_inputs: false,
+ initialized: false,
+ })
+ }
+
+ pub fn update(&mut self, best_bid: Option, best_ask: Option) {
+ self.has_inputs = true;
+ self.count += 1;
+
+ if let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) {
+ let smaller = std::cmp::min(best_bid, best_ask);
+ let larger = std::cmp::max(best_bid, best_ask);
+
+ let ratio = smaller.as_f64() / larger.as_f64();
+ self.value = ratio;
+
+ self.initialized = true;
+ }
+ // No market yet
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////////////
+#[cfg(test)]
+mod tests {
+ use nautilus_model::{
+ identifiers::instrument_id::InstrumentId,
+ stubs::{stub_order_book_mbp, stub_order_book_mbp_appl_xnas},
+ };
+ use rstest::rstest;
+
+ use super::*;
+
+ // TODO: Test `OrderBookMbo`: needs a good stub function
+
+ #[rstest]
+ fn test_initialized() {
+ let imbalance = BookImbalanceRatio::new().unwrap();
+ let display_str = format!("{imbalance}");
+ assert_eq!(display_str, "BookImbalanceRatio()");
+ assert_eq!(imbalance.value, 0.0);
+ assert_eq!(imbalance.count, 0);
+ assert!(!imbalance.has_inputs);
+ assert!(!imbalance.initialized);
+ }
+
+ #[rstest]
+ fn test_one_value_input_balanced() {
+ let mut imbalance = BookImbalanceRatio::new().unwrap();
+ let book = stub_order_book_mbp_appl_xnas();
+ imbalance.handle_book_mbp(&book);
+
+ assert_eq!(imbalance.count, 1);
+ assert_eq!(imbalance.value, 1.0);
+ assert!(imbalance.initialized);
+ assert!(imbalance.has_inputs);
+ }
+
+ #[rstest]
+ fn test_reset() {
+ let mut imbalance = BookImbalanceRatio::new().unwrap();
+ let book = stub_order_book_mbp_appl_xnas();
+ imbalance.handle_book_mbp(&book);
+ imbalance.reset();
+
+ assert_eq!(imbalance.count, 0);
+ assert_eq!(imbalance.value, 0.0);
+ assert!(!imbalance.initialized);
+ assert!(!imbalance.has_inputs);
+ }
+
+ #[rstest]
+ fn test_one_value_input_with_bid_imbalance() {
+ let mut imbalance = BookImbalanceRatio::new().unwrap();
+ let book = stub_order_book_mbp(
+ InstrumentId::from("AAPL.XNAS"),
+ 101.0,
+ 100.0,
+ 200.0, // <-- Larger bid side
+ 100.0,
+ 2,
+ 0.01,
+ 0,
+ 100.0,
+ 10,
+ );
+ imbalance.handle_book_mbp(&book);
+
+ assert_eq!(imbalance.count, 1);
+ assert_eq!(imbalance.value, 0.5);
+ assert!(imbalance.initialized);
+ assert!(imbalance.has_inputs);
+ }
+
+ #[rstest]
+ fn test_one_value_input_with_ask_imbalance() {
+ let mut imbalance = BookImbalanceRatio::new().unwrap();
+ let book = stub_order_book_mbp(
+ InstrumentId::from("AAPL.XNAS"),
+ 101.0,
+ 100.0,
+ 100.0,
+ 200.0, // <-- Larger ask side
+ 2,
+ 0.01,
+ 0,
+ 100.0,
+ 10,
+ );
+ imbalance.handle_book_mbp(&book);
+
+ assert_eq!(imbalance.count, 1);
+ assert_eq!(imbalance.value, 0.5);
+ assert!(imbalance.initialized);
+ assert!(imbalance.has_inputs);
+ }
+
+ #[rstest]
+ fn test_one_value_input_with_bid_imbalance_multiple_inputs() {
+ let mut imbalance = BookImbalanceRatio::new().unwrap();
+ let book = stub_order_book_mbp(
+ InstrumentId::from("AAPL.XNAS"),
+ 101.0,
+ 100.0,
+ 200.0, // <-- Larger bid side
+ 100.0,
+ 2,
+ 0.01,
+ 0,
+ 100.0,
+ 10,
+ );
+ imbalance.handle_book_mbp(&book);
+ imbalance.handle_book_mbp(&book);
+ imbalance.handle_book_mbp(&book);
+
+ assert_eq!(imbalance.count, 3);
+ assert_eq!(imbalance.value, 0.5);
+ assert!(imbalance.initialized);
+ assert!(imbalance.has_inputs);
+ }
+}
diff --git a/nautilus_core/indicators/src/book/mod.rs b/nautilus_core/indicators/src/book/mod.rs
new file mode 100644
index 000000000000..030ac78385ce
--- /dev/null
+++ b/nautilus_core/indicators/src/book/mod.rs
@@ -0,0 +1,16 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+pub mod imbalance;
diff --git a/nautilus_core/indicators/src/indicator.rs b/nautilus_core/indicators/src/indicator.rs
index 7041fdd3c82d..052acbf60d6c 100644
--- a/nautilus_core/indicators/src/indicator.rs
+++ b/nautilus_core/indicators/src/indicator.rs
@@ -15,20 +15,56 @@
use std::{fmt, fmt::Debug};
-use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick};
+use nautilus_model::{
+ data::{
+ bar::Bar, delta::OrderBookDelta, deltas::OrderBookDeltas, depth::OrderBookDepth10,
+ quote::QuoteTick, trade::TradeTick,
+ },
+ orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp},
+};
-/// Indicator trait
+const IMPL_ERR: &str = "is not implemented for";
+
+#[allow(unused_variables)]
pub trait Indicator {
fn name(&self) -> String;
fn has_inputs(&self) -> bool;
- fn is_initialized(&self) -> bool;
- fn handle_quote_tick(&mut self, tick: &QuoteTick);
- fn handle_trade_tick(&mut self, tick: &TradeTick);
- fn handle_bar(&mut self, bar: &Bar);
+ fn initialized(&self) -> bool;
+ fn handle_delta(&mut self, delta: &OrderBookDelta) {
+ // Eventually change this to log an error
+ panic!("`handle_delta` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_deltas(&mut self, deltas: &OrderBookDeltas) {
+ // Eventually change this to log an error
+ panic!("`handle_deltas` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_depth(&mut self, depth: &OrderBookDepth10) {
+ // Eventually change this to log an error
+ panic!("`handle_depth` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_book_mbo(&mut self, book: &OrderBookMbo) {
+ // Eventually change this to log an error
+ panic!("`handle_book_mbo` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_book_mbp(&mut self, book: &OrderBookMbp) {
+ // Eventually change this to log an error
+ panic!("`handle_book_mbp` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ // Eventually change this to log an error
+ panic!("`handle_quote_tick` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ // Eventually change this to log an error
+ panic!("`handle_trade_tick` {} `{}`", IMPL_ERR, self.name());
+ }
+ fn handle_bar(&mut self, bar: &Bar) {
+ // Eventually change this to log an error
+ panic!("`handle_bar` {} `{}`", IMPL_ERR, self.name());
+ }
fn reset(&mut self);
}
-/// Moving average trait
pub trait MovingAverage: Indicator {
fn value(&self) -> f64;
fn count(&self) -> usize;
@@ -37,14 +73,14 @@ pub trait MovingAverage: Indicator {
impl Debug for dyn Indicator + Send {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- // Implement custom formatting for the Indicator trait object.
+ // Implement custom formatting for the Indicator trait object
write!(f, "Indicator {{ ... }}")
}
}
impl Debug for dyn MovingAverage + Send {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- // Implement custom formatting for the Indicator trait object.
+ // Implement custom formatting for the Indicator trait object
write!(f, "MovingAverage()")
}
}
diff --git a/nautilus_core/indicators/src/lib.rs b/nautilus_core/indicators/src/lib.rs
index b34b5b1f3773..68ffc57e85b5 100644
--- a/nautilus_core/indicators/src/lib.rs
+++ b/nautilus_core/indicators/src/lib.rs
@@ -14,9 +14,11 @@
// -------------------------------------------------------------------------------------------------
pub mod average;
+pub mod book;
pub mod indicator;
pub mod momentum;
pub mod ratio;
+pub mod volatility;
#[cfg(test)]
mod stubs;
diff --git a/nautilus_core/indicators/src/momentum/aroon.rs b/nautilus_core/indicators/src/momentum/aroon.rs
index 1c42195fc2d5..970f38442161 100644
--- a/nautilus_core/indicators/src/momentum/aroon.rs
+++ b/nautilus_core/indicators/src/momentum/aroon.rs
@@ -16,7 +16,10 @@
use std::fmt::{Debug, Display};
use anyhow::Result;
-use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick};
+use nautilus_model::{
+ data::{bar::Bar, quote::QuoteTick, trade::TradeTick},
+ enums::PriceType,
+};
use pyo3::prelude::*;
use std::collections::VecDeque;
@@ -35,7 +38,7 @@ pub struct AroonOscillator {
pub aroon_down: f64,
pub value: f64,
pub count: usize,
- pub is_initialized: bool,
+ pub initialized: bool,
has_inputs: bool,
}
@@ -54,16 +57,18 @@ impl Indicator for AroonOscillator {
self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, _tick: &QuoteTick) {
- // Function body intentionally left blank.
+ fn handle_quote_tick(&mut self, tick: &QuoteTick) {
+ let price = tick.extract_price(PriceType::Mid).into();
+ self.update_raw(price, price);
}
- fn handle_trade_tick(&mut self, _tick: &TradeTick) {
- // Function body intentionally left blank.
+ fn handle_trade_tick(&mut self, tick: &TradeTick) {
+ let price = tick.price.into();
+ self.update_raw(price, price);
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -78,7 +83,7 @@ impl Indicator for AroonOscillator {
self.value = 0.0;
self.count = 0;
self.has_inputs = false;
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -93,7 +98,7 @@ impl AroonOscillator {
value: 0.0,
count: 0,
has_inputs: false,
- is_initialized: false,
+ initialized: false,
})
}
@@ -109,7 +114,7 @@ impl AroonOscillator {
self.low_inputs.push_front(low);
self.increment_count();
- if self.is_initialized {
+ if self.initialized {
// Makes sure we calculate with stable period
self.calculate_aroon();
}
@@ -150,10 +155,10 @@ impl AroonOscillator {
fn increment_count(&mut self) {
self.count += 1;
- if !self.is_initialized {
+ if !self.initialized {
self.has_inputs = true;
if self.count >= self.period {
- self.is_initialized = true;
+ self.initialized = true;
}
}
}
diff --git a/nautilus_core/indicators/src/momentum/cmo.rs b/nautilus_core/indicators/src/momentum/cmo.rs
new file mode 100644
index 000000000000..8fff4c3af949
--- /dev/null
+++ b/nautilus_core/indicators/src/momentum/cmo.rs
@@ -0,0 +1,207 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use std::fmt::Display;
+
+use anyhow::Result;
+use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick};
+use pyo3::prelude::*;
+
+use crate::{
+ average::{MovingAverageFactory, MovingAverageType},
+ indicator::{Indicator, MovingAverage},
+};
+
+#[repr(C)]
+#[derive(Debug)]
+#[pyclass(module = "nautilus_trader.core.nautilus.pyo3.indicators")]
+pub struct ChandeMomentumOscillator {
+ pub period: usize,
+ pub ma_type: MovingAverageType,
+ pub value: f64,
+ pub count: usize,
+ pub initialized: bool,
+ _previous_close: f64,
+ _average_gain: Box,
+ _average_loss: Box,
+ _has_inputs: bool,
+}
+
+impl Display for ChandeMomentumOscillator {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}({})", self.name(), self.period)
+ }
+}
+
+impl Indicator for ChandeMomentumOscillator {
+ fn name(&self) -> String {
+ stringify!(ChandeMomentumOscillator).to_string()
+ }
+
+ fn has_inputs(&self) -> bool {
+ self._has_inputs
+ }
+
+ fn initialized(&self) -> bool {
+ self.initialized
+ }
+
+ fn handle_quote_tick(&mut self, _tick: &QuoteTick) {
+ // Function body intentionally left blank.
+ }
+
+ fn handle_trade_tick(&mut self, _tick: &TradeTick) {
+ // Function body intentionally left blank.
+ }
+
+ fn handle_bar(&mut self, bar: &Bar) {
+ self.update_raw((&bar.close).into());
+ }
+
+ fn reset(&mut self) {
+ self.value = 0.0;
+ self.count = 0;
+ self._has_inputs = false;
+ self.initialized = false;
+ self._previous_close = 0.0;
+ }
+}
+
+impl ChandeMomentumOscillator {
+ pub fn new(period: usize, ma_type: Option) -> Result {
+ Ok(Self {
+ period,
+ ma_type: ma_type.unwrap_or(MovingAverageType::Wilder),
+ _average_gain: MovingAverageFactory::create(MovingAverageType::Wilder, period),
+ _average_loss: MovingAverageFactory::create(MovingAverageType::Wilder, period),
+ _previous_close: 0.0,
+ value: 0.0,
+ count: 0,
+ initialized: false,
+ _has_inputs: false,
+ })
+ }
+
+ pub fn update_raw(&mut self, close: f64) {
+ if !self._has_inputs {
+ self._previous_close = close;
+ self._has_inputs = true;
+ }
+
+ let gain: f64 = close - self._previous_close;
+ if gain > 0.0 {
+ self._average_gain.update_raw(gain);
+ self._average_loss.update_raw(0.0);
+ } else if gain < 0.0 {
+ self._average_gain.update_raw(0.0);
+ self._average_loss.update_raw(-gain);
+ } else {
+ self._average_gain.update_raw(0.0);
+ self._average_loss.update_raw(0.0);
+ }
+
+ if !self.initialized && self._average_gain.initialized() && self._average_loss.initialized()
+ {
+ self.initialized = true;
+ }
+ if self.initialized {
+ self.value = 100.0 * (self._average_gain.value() - self._average_loss.value())
+ / (self._average_gain.value() + self._average_loss.value());
+ }
+ self._previous_close = close;
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Tests
+////////////////////////////////////////////////////////////////////////////////
+#[cfg(test)]
+mod tests {
+ use nautilus_model::data::{bar::Bar, quote::QuoteTick};
+ use rstest::rstest;
+
+ use crate::{indicator::Indicator, momentum::cmo::ChandeMomentumOscillator, stubs::*};
+
+ #[rstest]
+ fn test_cmo_initialized(cmo_10: ChandeMomentumOscillator) {
+ let display_str = format!("{cmo_10}");
+ assert_eq!(display_str, "ChandeMomentumOscillator(10)");
+ assert_eq!(cmo_10.period, 10);
+ assert!(!cmo_10.initialized);
+ }
+
+ #[rstest]
+ fn test_initialized_with_required_inputs_returns_true(mut cmo_10: ChandeMomentumOscillator) {
+ for i in 0..12 {
+ cmo_10.update_raw(f64::from(i));
+ }
+ assert!(cmo_10.initialized);
+ }
+
+ #[rstest]
+ fn test_value_all_higher_inputs_returns_expected_value(mut cmo_10: ChandeMomentumOscillator) {
+ cmo_10.update_raw(109.93);
+ cmo_10.update_raw(110.0);
+ cmo_10.update_raw(109.77);
+ cmo_10.update_raw(109.96);
+ cmo_10.update_raw(110.29);
+ cmo_10.update_raw(110.53);
+ cmo_10.update_raw(110.27);
+ cmo_10.update_raw(110.21);
+ cmo_10.update_raw(110.06);
+ cmo_10.update_raw(110.19);
+ cmo_10.update_raw(109.83);
+ cmo_10.update_raw(109.9);
+ cmo_10.update_raw(110.0);
+ cmo_10.update_raw(110.03);
+ cmo_10.update_raw(110.13);
+ cmo_10.update_raw(109.95);
+ cmo_10.update_raw(109.75);
+ cmo_10.update_raw(110.15);
+ cmo_10.update_raw(109.9);
+ cmo_10.update_raw(110.04);
+ assert_eq!(cmo_10.value, 2.089_629_456_238_705_4);
+ }
+
+ #[rstest]
+ fn test_value_with_one_input_returns_expected_value(mut cmo_10: ChandeMomentumOscillator) {
+ cmo_10.update_raw(1.00000);
+ assert_eq!(cmo_10.value, 0.0);
+ }
+
+ #[rstest]
+ fn test_reset(mut cmo_10: ChandeMomentumOscillator) {
+ cmo_10.update_raw(1.00020);
+ cmo_10.update_raw(1.00030);
+ cmo_10.update_raw(1.00050);
+ cmo_10.reset();
+ assert!(!cmo_10.initialized());
+ assert_eq!(cmo_10.count, 0);
+ }
+
+ #[rstest]
+ fn test_handle_quote_tick(mut cmo_10: ChandeMomentumOscillator, quote_tick: QuoteTick) {
+ cmo_10.handle_quote_tick("e_tick);
+ assert_eq!(cmo_10.count, 0);
+ assert_eq!(cmo_10.value, 0.0);
+ }
+
+ #[rstest]
+ fn test_handle_bar(mut cmo_10: ChandeMomentumOscillator, bar_ethusdt_binance_minute_bid: Bar) {
+ cmo_10.handle_bar(&bar_ethusdt_binance_minute_bid);
+ assert_eq!(cmo_10.count, 0);
+ assert_eq!(cmo_10.value, 0.0);
+ }
+}
diff --git a/nautilus_core/indicators/src/momentum/mod.rs b/nautilus_core/indicators/src/momentum/mod.rs
index 35fd3d83c086..daf02fc72965 100644
--- a/nautilus_core/indicators/src/momentum/mod.rs
+++ b/nautilus_core/indicators/src/momentum/mod.rs
@@ -14,4 +14,5 @@
// -------------------------------------------------------------------------------------------------
pub mod aroon;
+pub mod cmo;
pub mod rsi;
diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs
index 07623dbee708..0abedf6b0ef0 100644
--- a/nautilus_core/indicators/src/momentum/rsi.rs
+++ b/nautilus_core/indicators/src/momentum/rsi.rs
@@ -36,12 +36,12 @@ pub struct RelativeStrengthIndex {
pub ma_type: MovingAverageType,
pub value: f64,
pub count: usize,
- pub is_initialized: bool,
- _has_inputs: bool,
- _last_value: f64,
- _average_gain: Box,
- _average_loss: Box,
- _rsi_max: f64,
+ pub initialized: bool,
+ has_inputs: bool,
+ last_value: f64,
+ average_gain: Box,
+ average_loss: Box,
+ rsi_max: f64,
}
impl Display for RelativeStrengthIndex {
@@ -56,19 +56,19 @@ impl Indicator for RelativeStrengthIndex {
}
fn has_inputs(&self) -> bool {
- self._has_inputs
+ self.has_inputs
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(PriceType::Mid).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(PriceType::Mid).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -77,10 +77,10 @@ impl Indicator for RelativeStrengthIndex {
fn reset(&mut self) {
self.value = 0.0;
- self._last_value = 0.0;
+ self.last_value = 0.0;
self.count = 0;
- self._has_inputs = false;
- self.is_initialized = false;
+ self.has_inputs = false;
+ self.initialized = false;
}
}
@@ -90,53 +90,50 @@ impl RelativeStrengthIndex {
period,
ma_type: ma_type.unwrap_or(MovingAverageType::Exponential),
value: 0.0,
- _last_value: 0.0,
+ last_value: 0.0,
count: 0,
// inputs: Vec::new(),
- _has_inputs: false,
- _average_gain: MovingAverageFactory::create(MovingAverageType::Exponential, period),
- _average_loss: MovingAverageFactory::create(MovingAverageType::Exponential, period),
- _rsi_max: 1.0,
- is_initialized: false,
+ has_inputs: false,
+ average_gain: MovingAverageFactory::create(MovingAverageType::Exponential, period),
+ average_loss: MovingAverageFactory::create(MovingAverageType::Exponential, period),
+ rsi_max: 1.0,
+ initialized: false,
})
}
pub fn update_raw(&mut self, value: f64) {
- if !self._has_inputs {
- self._last_value = value;
- self._has_inputs = true;
+ if !self.has_inputs {
+ self.last_value = value;
+ self.has_inputs = true;
}
- let gain = value - self._last_value;
+ let gain = value - self.last_value;
if gain > 0.0 {
- self._average_gain.update_raw(gain);
- self._average_loss.update_raw(0.0);
+ self.average_gain.update_raw(gain);
+ self.average_loss.update_raw(0.0);
} else if gain < 0.0 {
- self._average_loss.update_raw(-gain);
- self._average_gain.update_raw(0.0);
+ self.average_loss.update_raw(-gain);
+ self.average_gain.update_raw(0.0);
} else {
- self._average_loss.update_raw(0.0);
- self._average_gain.update_raw(0.0);
+ self.average_loss.update_raw(0.0);
+ self.average_gain.update_raw(0.0);
}
// init count from average gain MA
- self.count = self._average_gain.count();
- if !self.is_initialized
- && self._average_loss.is_initialized()
- && self._average_gain.is_initialized()
- {
- self.is_initialized = true;
+ self.count = self.average_gain.count();
+ if !self.initialized && self.average_loss.initialized() && self.average_gain.initialized() {
+ self.initialized = true;
}
- if self._average_loss.value() == 0.0 {
- self.value = self._rsi_max;
+ if self.average_loss.value() == 0.0 {
+ self.value = self.rsi_max;
return;
}
- let rs = self._average_gain.value() / self._average_loss.value();
- self.value = self._rsi_max - (self._rsi_max / (1.0 + rs));
- self._last_value = value;
+ let rs = self.average_gain.value() / self.average_loss.value();
+ self.value = self.rsi_max - (self.rsi_max / (1.0 + rs));
+ self.last_value = value;
- if !self.is_initialized && self.count >= self.period {
- self.is_initialized = true;
+ if !self.initialized && self.count >= self.period {
+ self.initialized = true;
}
}
}
@@ -156,7 +153,7 @@ mod tests {
let display_str = format!("{rsi_10}");
assert_eq!(display_str, "RelativeStrengthIndex(10,EXPONENTIAL)");
assert_eq!(rsi_10.period, 10);
- assert!(!rsi_10.is_initialized);
+ assert!(!rsi_10.initialized);
}
#[rstest]
@@ -164,7 +161,7 @@ mod tests {
for i in 0..12 {
rsi_10.update_raw(f64::from(i));
}
- assert!(rsi_10.is_initialized);
+ assert!(rsi_10.initialized);
}
#[rstest]
@@ -220,7 +217,7 @@ mod tests {
rsi_10.update_raw(1.0);
rsi_10.update_raw(2.0);
rsi_10.reset();
- assert!(!rsi_10.is_initialized());
+ assert!(!rsi_10.initialized());
assert_eq!(rsi_10.count, 0);
}
diff --git a/nautilus_core/indicators/src/python/average/ama.rs b/nautilus_core/indicators/src/python/average/ama.rs
index 082f625d7e56..7710e692adc2 100644
--- a/nautilus_core/indicators/src/python/average/ama.rs
+++ b/nautilus_core/indicators/src/python/average/ama.rs
@@ -43,6 +43,16 @@ impl AdaptiveMovingAverage {
.map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!(
+ "WeightedMovingAverage({}({},{},{})",
+ self.name(),
+ self.period_efficiency_ratio,
+ self.period_fast,
+ self.period_slow
+ )
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -64,7 +74,7 @@ impl AdaptiveMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -91,14 +101,4 @@ impl AdaptiveMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!(
- "WeightedMovingAverage({}({},{},{})",
- self.name(),
- self.period_efficiency_ratio,
- self.period_fast,
- self.period_slow
- )
- }
}
diff --git a/nautilus_core/indicators/src/python/average/dema.rs b/nautilus_core/indicators/src/python/average/dema.rs
index c26d4a147bbc..b3b26c7c2d8e 100644
--- a/nautilus_core/indicators/src/python/average/dema.rs
+++ b/nautilus_core/indicators/src/python/average/dema.rs
@@ -32,6 +32,10 @@ impl DoubleExponentialMovingAverage {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("DoubleExponentialMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -65,7 +69,7 @@ impl DoubleExponentialMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -92,8 +96,4 @@ impl DoubleExponentialMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("DoubleExponentialMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/average/ema.rs b/nautilus_core/indicators/src/python/average/ema.rs
index 4a8e35c2d3ef..71cb1e67e414 100644
--- a/nautilus_core/indicators/src/python/average/ema.rs
+++ b/nautilus_core/indicators/src/python/average/ema.rs
@@ -32,6 +32,10 @@ impl ExponentialMovingAverage {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("ExponentialMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -71,7 +75,7 @@ impl ExponentialMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -98,8 +102,4 @@ impl ExponentialMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("ExponentialMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/average/hma.rs b/nautilus_core/indicators/src/python/average/hma.rs
index 4ba74e77bdab..3b4ad4967a6a 100644
--- a/nautilus_core/indicators/src/python/average/hma.rs
+++ b/nautilus_core/indicators/src/python/average/hma.rs
@@ -32,6 +32,10 @@ impl HullMovingAverage {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("HullMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -65,7 +69,7 @@ impl HullMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -92,8 +96,4 @@ impl HullMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("HullMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/average/rma.rs b/nautilus_core/indicators/src/python/average/rma.rs
index a126f606af5b..f1bef3f6de2d 100644
--- a/nautilus_core/indicators/src/python/average/rma.rs
+++ b/nautilus_core/indicators/src/python/average/rma.rs
@@ -32,6 +32,10 @@ impl WilderMovingAverage {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("WilderMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -71,7 +75,7 @@ impl WilderMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -98,8 +102,4 @@ impl WilderMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("WilderMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/average/sma.rs b/nautilus_core/indicators/src/python/average/sma.rs
index 4b4bd6a0fb9c..b9f6df39edb2 100644
--- a/nautilus_core/indicators/src/python/average/sma.rs
+++ b/nautilus_core/indicators/src/python/average/sma.rs
@@ -32,6 +32,10 @@ impl SimpleMovingAverage {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("SimpleMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -65,7 +69,7 @@ impl SimpleMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -92,8 +96,4 @@ impl SimpleMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("SimpleMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/average/wma.rs b/nautilus_core/indicators/src/python/average/wma.rs
index ef234e4e2cee..ca500ae67f41 100644
--- a/nautilus_core/indicators/src/python/average/wma.rs
+++ b/nautilus_core/indicators/src/python/average/wma.rs
@@ -36,6 +36,10 @@ impl WeightedMovingAverage {
Self::new(period, weights, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("WeightedMovingAverage({},{:?})", self.period, self.weights)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -63,7 +67,7 @@ impl WeightedMovingAverage {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "handle_quote_tick")]
@@ -90,8 +94,4 @@ impl WeightedMovingAverage {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("WeightedMovingAverage({},{:?})", self.period, self.weights)
- }
}
diff --git a/nautilus_core/indicators/src/python/book/imbalance.rs b/nautilus_core/indicators/src/python/book/imbalance.rs
new file mode 100644
index 000000000000..cecb37436abf
--- /dev/null
+++ b/nautilus_core/indicators/src/python/book/imbalance.rs
@@ -0,0 +1,85 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use nautilus_core::python::to_pyvalue_err;
+use nautilus_model::{
+ orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp},
+ types::quantity::Quantity,
+};
+use pyo3::prelude::*;
+
+use crate::{book::imbalance::BookImbalanceRatio, indicator::Indicator};
+
+#[pymethods]
+impl BookImbalanceRatio {
+ #[new]
+ fn py_new() -> PyResult {
+ Self::new().map_err(to_pyvalue_err)
+ }
+
+ fn __repr__(&self) -> String {
+ self.to_string()
+ }
+
+ #[getter]
+ #[pyo3(name = "name")]
+ fn py_name(&self) -> String {
+ self.name()
+ }
+
+ #[getter]
+ #[pyo3(name = "count")]
+ fn py_count(&self) -> usize {
+ self.count
+ }
+
+ #[getter]
+ #[pyo3(name = "value")]
+ fn py_value(&self) -> f64 {
+ self.value
+ }
+
+ #[getter]
+ #[pyo3(name = "has_inputs")]
+ fn py_has_inputs(&self) -> bool {
+ self.has_inputs()
+ }
+
+ #[getter]
+ #[pyo3(name = "initialized")]
+ fn py_initialized(&self) -> bool {
+ self.initialized
+ }
+
+ #[pyo3(name = "handle_book_mbo")]
+ fn py_handle_book_mbo(&mut self, book: &OrderBookMbo) {
+ self.handle_book_mbo(book);
+ }
+
+ #[pyo3(name = "handle_book_mbp")]
+ fn py_handle_book_mbp(&mut self, book: &OrderBookMbp) {
+ self.handle_book_mbp(book);
+ }
+
+ #[pyo3(name = "update")]
+ fn py_update(&mut self, best_bid: Option, best_ask: Option) {
+ self.update(best_bid, best_ask);
+ }
+
+ #[pyo3(name = "reset")]
+ fn py_reset(&mut self) {
+ self.reset();
+ }
+}
diff --git a/nautilus_core/indicators/src/python/book/mod.rs b/nautilus_core/indicators/src/python/book/mod.rs
new file mode 100644
index 000000000000..030ac78385ce
--- /dev/null
+++ b/nautilus_core/indicators/src/python/book/mod.rs
@@ -0,0 +1,16 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+pub mod imbalance;
diff --git a/nautilus_core/indicators/src/python/mod.rs b/nautilus_core/indicators/src/python/mod.rs
index 032d188a8900..ca7b1da5cbe8 100644
--- a/nautilus_core/indicators/src/python/mod.rs
+++ b/nautilus_core/indicators/src/python/mod.rs
@@ -16,8 +16,10 @@
use pyo3::{prelude::*, pymodule};
pub mod average;
+pub mod book;
pub mod momentum;
pub mod ratio;
+pub mod volatility;
#[pymodule]
pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> {
@@ -28,10 +30,15 @@ pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
+ // book
+ m.add_class::()?;
// ratio
m.add_class::()?;
// momentum
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
+ // volatility
+ m.add_class::()?;
Ok(())
}
diff --git a/nautilus_core/indicators/src/python/momentum/aroon.rs b/nautilus_core/indicators/src/python/momentum/aroon.rs
index 74eb76cd0493..5ef64bfe2193 100644
--- a/nautilus_core/indicators/src/python/momentum/aroon.rs
+++ b/nautilus_core/indicators/src/python/momentum/aroon.rs
@@ -26,6 +26,10 @@ impl AroonOscillator {
Self::new(period).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("AroonOscillator({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -71,7 +75,7 @@ impl AroonOscillator {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "update_raw")]
@@ -96,10 +100,6 @@ impl AroonOscillator {
#[pyo3(name = "reset")]
fn py_reset(&mut self) {
- self.reset()
- }
-
- fn __repr__(&self) -> String {
- format!("AroonOscillator({})", self.period)
+ self.reset();
}
}
diff --git a/nautilus_core/indicators/src/python/momentum/cmo.rs b/nautilus_core/indicators/src/python/momentum/cmo.rs
new file mode 100644
index 000000000000..37e7915671c1
--- /dev/null
+++ b/nautilus_core/indicators/src/python/momentum/cmo.rs
@@ -0,0 +1,95 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use nautilus_core::python::to_pyvalue_err;
+use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick};
+use pyo3::prelude::*;
+
+use crate::{
+ average::MovingAverageType, indicator::Indicator, momentum::cmo::ChandeMomentumOscillator,
+};
+
+#[pymethods]
+impl ChandeMomentumOscillator {
+ #[new]
+ pub fn py_new(period: usize, ma_type: Option) -> PyResult {
+ Self::new(period, ma_type).map_err(to_pyvalue_err)
+ }
+
+ #[getter]
+ #[pyo3(name = "name")]
+ fn py_name(&self) -> String {
+ self.name()
+ }
+
+ #[getter]
+ #[pyo3(name = "period")]
+ fn py_period(&self) -> usize {
+ self.period
+ }
+
+ #[getter]
+ #[pyo3(name = "has_inputs")]
+ fn py_has_inputs(&self) -> bool {
+ self.has_inputs()
+ }
+
+ #[getter]
+ #[pyo3(name = "count")]
+ fn py_count(&self) -> usize {
+ self.count
+ }
+
+ #[getter]
+ #[pyo3(name = "value")]
+ fn py_value(&self) -> f64 {
+ self.value
+ }
+
+ #[getter]
+ #[pyo3(name = "initialized")]
+ fn py_initialized(&self) -> bool {
+ self.initialized
+ }
+
+ #[pyo3(name = "update_raw")]
+ fn py_update_raw(&mut self, close: f64) {
+ self.update_raw(close);
+ }
+
+ #[pyo3(name = "handle_quote_tick")]
+ fn py_handle_quote_tick(&mut self, _tick: &QuoteTick) {
+ // Function body intentionally left blank.
+ }
+
+ #[pyo3(name = "handle_trade_tick")]
+ fn py_handle_trade_tick(&mut self, _tick: &TradeTick) {
+ // Function body intentionally left blank.
+ }
+
+ #[pyo3(name = "handle_bar")]
+ fn py_handle_bar(&mut self, bar: &Bar) {
+ self.update_raw((&bar.close).into());
+ }
+
+ #[pyo3(name = "reset")]
+ fn py_reset(&mut self) {
+ self.reset()
+ }
+
+ fn __repr__(&self) -> String {
+ format!("ChandeMomentumOscillator({})", self.period)
+ }
+}
diff --git a/nautilus_core/indicators/src/python/momentum/mod.rs b/nautilus_core/indicators/src/python/momentum/mod.rs
index 35fd3d83c086..daf02fc72965 100644
--- a/nautilus_core/indicators/src/python/momentum/mod.rs
+++ b/nautilus_core/indicators/src/python/momentum/mod.rs
@@ -14,4 +14,5 @@
// -------------------------------------------------------------------------------------------------
pub mod aroon;
+pub mod cmo;
pub mod rsi;
diff --git a/nautilus_core/indicators/src/python/momentum/rsi.rs b/nautilus_core/indicators/src/python/momentum/rsi.rs
index fa0b1798da54..60ba94c2ca5a 100644
--- a/nautilus_core/indicators/src/python/momentum/rsi.rs
+++ b/nautilus_core/indicators/src/python/momentum/rsi.rs
@@ -31,6 +31,10 @@ impl RelativeStrengthIndex {
Self::new(period, ma_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("ExponentialMovingAverage({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -58,7 +62,7 @@ impl RelativeStrengthIndex {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "update_raw")]
@@ -80,8 +84,4 @@ impl RelativeStrengthIndex {
fn py_handle_trade_tick(&mut self, tick: &TradeTick) {
self.update_raw((&tick.price).into());
}
-
- fn __repr__(&self) -> String {
- format!("ExponentialMovingAverage({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs
index 33475e26882d..6c6c523f3800 100644
--- a/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs
+++ b/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs
@@ -26,6 +26,10 @@ impl EfficiencyRatio {
Self::new(period, price_type).map_err(to_pyvalue_err)
}
+ fn __repr__(&self) -> String {
+ format!("EfficiencyRatio({})", self.period)
+ }
+
#[getter]
#[pyo3(name = "name")]
fn py_name(&self) -> String {
@@ -47,7 +51,7 @@ impl EfficiencyRatio {
#[getter]
#[pyo3(name = "initialized")]
fn py_initialized(&self) -> bool {
- self.is_initialized
+ self.initialized
}
#[pyo3(name = "has_inputs")]
@@ -59,8 +63,4 @@ impl EfficiencyRatio {
fn py_update_raw(&mut self, value: f64) {
self.update_raw(value);
}
-
- fn __repr__(&self) -> String {
- format!("EfficiencyRatio({})", self.period)
- }
}
diff --git a/nautilus_core/indicators/src/python/volatility/atr.rs b/nautilus_core/indicators/src/python/volatility/atr.rs
new file mode 100644
index 000000000000..28daaff2efef
--- /dev/null
+++ b/nautilus_core/indicators/src/python/volatility/atr.rs
@@ -0,0 +1,101 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use nautilus_core::python::to_pyvalue_err;
+use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick};
+use pyo3::prelude::*;
+
+use crate::{average::MovingAverageType, indicator::Indicator, volatility::atr::AverageTrueRange};
+
+#[pymethods]
+impl AverageTrueRange {
+ #[new]
+ pub fn py_new(
+ period: usize,
+ ma_type: Option,
+ use_previous: Option,
+ value_floor: Option,
+ ) -> PyResult {
+ Self::new(period, ma_type, use_previous, value_floor).map_err(to_pyvalue_err)
+ }
+
+ fn __repr__(&self) -> String {
+ format!(
+ "AverageTrueRange({},{},{},{})",
+ self.period, self.ma_type, self.use_previous, self.value_floor,
+ )
+ }
+
+ #[getter]
+ #[pyo3(name = "name")]
+ fn py_name(&self) -> String {
+ self.name()
+ }
+
+ #[getter]
+ #[pyo3(name = "period")]
+ fn py_period(&self) -> usize {
+ self.period
+ }
+
+ #[getter]
+ #[pyo3(name = "has_inputs")]
+ fn py_has_inputs(&self) -> bool {
+ self.has_inputs()
+ }
+
+ #[getter]
+ #[pyo3(name = "count")]
+ fn py_count(&self) -> usize {
+ self.count
+ }
+
+ #[getter]
+ #[pyo3(name = "value")]
+ fn py_value(&self) -> f64 {
+ self.value
+ }
+
+ #[getter]
+ #[pyo3(name = "initialized")]
+ fn py_initialized(&self) -> bool {
+ self.initialized
+ }
+
+ #[pyo3(name = "update_raw")]
+ fn py_update_raw(&mut self, high: f64, low: f64, close: f64) {
+ self.update_raw(high, low, close);
+ }
+
+ #[pyo3(name = "handle_quote_tick")]
+ fn py_handle_quote_tick(&mut self, _tick: &QuoteTick) {
+ // Function body intentionally left blank.
+ }
+
+ #[pyo3(name = "handle_trade_tick")]
+ fn py_handle_trade_tick(&mut self, _tick: &TradeTick) {
+ // Function body intentionally left blank.
+ }
+
+ #[pyo3(name = "handle_bar")]
+ fn py_handle_bar(&mut self, bar: &Bar) {
+ self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
+ }
+
+ #[pyo3(name = "reset")]
+ fn py_reset(&mut self) {
+ self.reset();
+ }
+}
diff --git a/nautilus_core/indicators/src/python/volatility/mod.rs b/nautilus_core/indicators/src/python/volatility/mod.rs
new file mode 100644
index 000000000000..799b0bb38a10
--- /dev/null
+++ b/nautilus_core/indicators/src/python/volatility/mod.rs
@@ -0,0 +1,16 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+pub mod atr;
diff --git a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs
index 6735969a0045..012a6f96b2b7 100644
--- a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs
+++ b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs
@@ -36,8 +36,8 @@ pub struct EfficiencyRatio {
pub price_type: PriceType,
pub value: f64,
pub inputs: Vec,
- pub is_initialized: bool,
- _deltas: Vec,
+ pub initialized: bool,
+ deltas: Vec,
}
impl Display for EfficiencyRatio {
@@ -54,16 +54,16 @@ impl Indicator for EfficiencyRatio {
fn has_inputs(&self) -> bool {
!self.inputs.is_empty()
}
- fn is_initialized(&self) -> bool {
- self.is_initialized
+ fn initialized(&self) -> bool {
+ self.initialized
}
- fn handle_quote_tick(&mut self, tick: &QuoteTick) {
- self.update_raw(tick.extract_price(self.price_type).into());
+ fn handle_quote_tick(&mut self, quote: &QuoteTick) {
+ self.update_raw(quote.extract_price(self.price_type).into());
}
- fn handle_trade_tick(&mut self, tick: &TradeTick) {
- self.update_raw((&tick.price).into());
+ fn handle_trade_tick(&mut self, trade: &TradeTick) {
+ self.update_raw((&trade.price).into());
}
fn handle_bar(&mut self, bar: &Bar) {
@@ -73,7 +73,7 @@ impl Indicator for EfficiencyRatio {
fn reset(&mut self) {
self.value = 0.0;
self.inputs.clear();
- self.is_initialized = false;
+ self.initialized = false;
}
}
@@ -84,8 +84,8 @@ impl EfficiencyRatio {
price_type: price_type.unwrap_or(PriceType::Last),
value: 0.0,
inputs: Vec::with_capacity(period),
- _deltas: Vec::with_capacity(period),
- is_initialized: false,
+ deltas: Vec::with_capacity(period),
+ initialized: false,
})
}
@@ -94,13 +94,13 @@ impl EfficiencyRatio {
if self.inputs.len() < 2 {
self.value = 0.0;
return;
- } else if !self.is_initialized && self.inputs.len() >= self.period {
- self.is_initialized = true;
+ } else if !self.initialized && self.inputs.len() >= self.period {
+ self.initialized = true;
}
let last_diff =
(self.inputs[self.inputs.len() - 1] - self.inputs[self.inputs.len() - 2]).abs();
- self._deltas.push(last_diff);
- let sum_deltas = self._deltas.iter().sum::().abs();
+ self.deltas.push(last_diff);
+ let sum_deltas = self.deltas.iter().sum::().abs();
let net_diff = (self.inputs[self.inputs.len() - 1] - self.inputs[0]).abs();
self.value = if sum_deltas == 0.0 {
0.0
@@ -125,7 +125,7 @@ mod tests {
let display_str = format!("{efficiency_ratio_10}");
assert_eq!(display_str, "EfficiencyRatio(10)");
assert_eq!(efficiency_ratio_10.period, 10);
- assert!(!efficiency_ratio_10.is_initialized);
+ assert!(!efficiency_ratio_10.initialized);
}
#[rstest]
@@ -134,10 +134,10 @@ mod tests {
efficiency_ratio_10.update_raw(f64::from(i));
}
assert_eq!(efficiency_ratio_10.inputs.len(), 9);
- assert!(!efficiency_ratio_10.is_initialized);
+ assert!(!efficiency_ratio_10.initialized);
efficiency_ratio_10.update_raw(1.0);
assert_eq!(efficiency_ratio_10.inputs.len(), 10);
- assert!(efficiency_ratio_10.is_initialized);
+ assert!(efficiency_ratio_10.initialized);
}
#[rstest]
@@ -203,9 +203,9 @@ mod tests {
for i in 1..=10 {
efficiency_ratio_10.update_raw(f64::from(i));
}
- assert!(efficiency_ratio_10.is_initialized);
+ assert!(efficiency_ratio_10.initialized);
efficiency_ratio_10.reset();
- assert!(!efficiency_ratio_10.is_initialized);
+ assert!(!efficiency_ratio_10.initialized);
assert_eq!(efficiency_ratio_10.value, 0.0);
}
diff --git a/nautilus_core/indicators/src/stubs.rs b/nautilus_core/indicators/src/stubs.rs
index a4450369ebc4..53014cae7566 100644
--- a/nautilus_core/indicators/src/stubs.rs
+++ b/nautilus_core/indicators/src/stubs.rs
@@ -31,7 +31,7 @@ use crate::{
ema::ExponentialMovingAverage, hma::HullMovingAverage, rma::WilderMovingAverage,
sma::SimpleMovingAverage, wma::WeightedMovingAverage, MovingAverageType,
},
- momentum::rsi::RelativeStrengthIndex,
+ momentum::{cmo::ChandeMomentumOscillator, rsi::RelativeStrengthIndex},
ratio::efficiency_ratio::EfficiencyRatio,
};
@@ -149,3 +149,8 @@ pub fn efficiency_ratio_10() -> EfficiencyRatio {
pub fn rsi_10() -> RelativeStrengthIndex {
RelativeStrengthIndex::new(10, Some(MovingAverageType::Exponential)).unwrap()
}
+
+#[fixture]
+pub fn cmo_10() -> ChandeMomentumOscillator {
+ ChandeMomentumOscillator::new(10, Some(MovingAverageType::Wilder)).unwrap()
+}
diff --git a/nautilus_core/indicators/src/volatility/atr.rs b/nautilus_core/indicators/src/volatility/atr.rs
new file mode 100644
index 000000000000..935ad2ecae5a
--- /dev/null
+++ b/nautilus_core/indicators/src/volatility/atr.rs
@@ -0,0 +1,141 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use std::fmt::{Debug, Display};
+
+use anyhow::Result;
+use nautilus_model::data::bar::Bar;
+use pyo3::prelude::*;
+
+use crate::{
+ average::{MovingAverageFactory, MovingAverageType},
+ indicator::{Indicator, MovingAverage},
+};
+
+/// An indicator which calculates a Average True Range (ATR) across a rolling window.
+#[repr(C)]
+#[derive(Debug)]
+#[pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")]
+pub struct AverageTrueRange {
+ pub period: usize,
+ pub ma_type: MovingAverageType,
+ pub use_previous: bool,
+ pub value_floor: f64,
+ pub value: f64,
+ pub count: usize,
+ pub initialized: bool,
+ ma: Box,
+ has_inputs: bool,
+ previous_close: f64,
+}
+
+impl Display for AverageTrueRange {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}({},{},{},{})",
+ self.name(),
+ self.period,
+ self.ma_type,
+ self.use_previous,
+ self.value_floor,
+ )
+ }
+}
+
+impl Indicator for AverageTrueRange {
+ fn name(&self) -> String {
+ stringify!(AverageTrueRange).to_string()
+ }
+
+ fn has_inputs(&self) -> bool {
+ self.has_inputs
+ }
+
+ fn initialized(&self) -> bool {
+ self.initialized
+ }
+
+ fn handle_bar(&mut self, bar: &Bar) {
+ self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
+ }
+
+ fn reset(&mut self) {
+ self.previous_close = 0.0;
+ self.value = 0.0;
+ self.count = 0;
+ self.has_inputs = false;
+ self.initialized = false;
+ }
+}
+
+impl AverageTrueRange {
+ pub fn new(
+ period: usize,
+ ma_type: Option,
+ use_previous: Option,
+ value_floor: Option,
+ ) -> Result {
+ Ok(Self {
+ period,
+ ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
+ use_previous: use_previous.unwrap_or(true),
+ value_floor: value_floor.unwrap_or(0.0),
+ value: 0.0,
+ count: 0,
+ previous_close: 0.0,
+ ma: MovingAverageFactory::create(MovingAverageType::Simple, period),
+ has_inputs: false,
+ initialized: false,
+ })
+ }
+
+ pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
+ if self.use_previous {
+ if !self.has_inputs {
+ self.previous_close = close;
+ }
+ self.ma.update_raw(
+ f64::max(self.previous_close, high) - f64::min(low, self.previous_close),
+ );
+ self.previous_close = close;
+ } else {
+ self.ma.update_raw(high - low);
+ }
+
+ self._floor_value();
+ self.increment_count();
+ }
+
+ fn _floor_value(&mut self) {
+ if self.value_floor == 0.0 || self.value_floor < self.ma.value() {
+ self.value = self.ma.value();
+ } else {
+ // Floor the value
+ self.value = self.value_floor;
+ }
+ }
+
+ fn increment_count(&mut self) {
+ self.count += 1;
+
+ if !self.initialized {
+ self.has_inputs = true;
+ if self.count >= self.period {
+ self.initialized = true;
+ }
+ }
+ }
+}
diff --git a/nautilus_core/indicators/src/volatility/mod.rs b/nautilus_core/indicators/src/volatility/mod.rs
new file mode 100644
index 000000000000..799b0bb38a10
--- /dev/null
+++ b/nautilus_core/indicators/src/volatility/mod.rs
@@ -0,0 +1,16 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+pub mod atr;
diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml
index 6ab77756e2d5..eba1f63c494f 100644
--- a/nautilus_core/model/Cargo.toml
+++ b/nautilus_core/model/Cargo.toml
@@ -26,7 +26,7 @@ thiserror = { workspace = true }
thousands = { workspace = true }
ustr = { workspace = true }
chrono = { workspace = true }
-derive_builder = "0.13.0"
+derive_builder = "0.13.1"
evalexpr = "11.3.0"
tabled = "0.15.0"
diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs
index bf021dd2b0ca..290ad06c933d 100644
--- a/nautilus_core/model/src/data/bar.rs
+++ b/nautilus_core/model/src/data/bar.rs
@@ -38,7 +38,7 @@ use crate::{
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct BarSpecification {
@@ -73,7 +73,7 @@ impl Display for BarSpecification {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
pub struct BarType {
/// The bar types instrument ID.
@@ -206,7 +206,7 @@ impl<'de> Deserialize<'de> for BarType {
#[serde(tag = "type")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
pub struct Bar {
/// The bar type for this bar.
diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs
index 7bf17b21e628..6ac0b3db3be6 100644
--- a/nautilus_core/model/src/data/delta.rs
+++ b/nautilus_core/model/src/data/delta.rs
@@ -38,7 +38,7 @@ use crate::{
#[serde(tag = "type")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct OrderBookDelta {
diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs
index c6a66745be22..1b444662e8e9 100644
--- a/nautilus_core/model/src/data/deltas.rs
+++ b/nautilus_core/model/src/data/deltas.rs
@@ -13,20 +13,23 @@
// limitations under the License.
// -------------------------------------------------------------------------------------------------
-use std::fmt::{Display, Formatter};
+use std::{
+ fmt::{Display, Formatter},
+ hash::{Hash, Hasher},
+};
use nautilus_core::time::UnixNanos;
-use pyo3::prelude::*;
use super::delta::OrderBookDelta;
use crate::identifiers::instrument_id::InstrumentId;
/// Represents a grouped batch of `OrderBookDelta` updates for an `OrderBook`.
-#[repr(C)]
+///
+/// This type cannot be `repr(C)` due to the `deltas` vec.
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
pub struct OrderBookDeltas {
/// The instrument ID for the book.
@@ -46,14 +49,14 @@ pub struct OrderBookDeltas {
impl OrderBookDeltas {
#[allow(clippy::too_many_arguments)]
#[must_use]
- pub fn new(
- instrument_id: InstrumentId,
- deltas: Vec,
- flags: u8,
- sequence: u64,
- ts_event: UnixNanos,
- ts_init: UnixNanos,
- ) -> Self {
+ pub fn new(instrument_id: InstrumentId, deltas: Vec) -> Self {
+ assert!(!deltas.is_empty(), "`deltas` cannot be empty");
+ // SAFETY: We asserted `deltas` is not empty
+ let last = deltas.last().unwrap();
+ let flags = last.flags;
+ let sequence = last.sequence;
+ let ts_event = last.ts_event;
+ let ts_init = last.ts_init;
Self {
instrument_id,
deltas,
@@ -65,7 +68,22 @@ impl OrderBookDeltas {
}
}
-// TODO: Potentially implement later
+impl PartialEq for OrderBookDeltas {
+ fn eq(&self, other: &Self) -> bool {
+ self.instrument_id == other.instrument_id && self.sequence == other.sequence
+ }
+}
+
+impl Eq for OrderBookDeltas {}
+
+impl Hash for OrderBookDeltas {
+ fn hash(&self, state: &mut H) {
+ self.instrument_id.hash(state);
+ self.sequence.hash(state);
+ }
+}
+
+// TODO: Implement
// impl Serializable for OrderBookDeltas {}
// TODO: Exact format for Debug and Display TBD
@@ -195,7 +213,7 @@ pub mod stubs {
let deltas = vec![delta0, delta1, delta2, delta3, delta4, delta5, delta6];
- OrderBookDeltas::new(instrument_id, deltas, flags, sequence, ts_event, ts_init)
+ OrderBookDeltas::new(instrument_id, deltas)
}
}
@@ -310,10 +328,6 @@ mod tests {
let deltas = OrderBookDeltas::new(
instrument_id,
vec![delta0, delta1, delta2, delta3, delta4, delta5, delta6],
- flags,
- sequence,
- ts_event,
- ts_init,
);
assert_eq!(deltas.instrument_id, instrument_id);
diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs
index 5c15f0f3166f..619976ed0290 100644
--- a/nautilus_core/model/src/data/depth.rs
+++ b/nautilus_core/model/src/data/depth.rs
@@ -20,7 +20,6 @@ use std::{
use indexmap::IndexMap;
use nautilus_core::{serialization::Serializable, time::UnixNanos};
-use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use super::order::BookOrder;
@@ -41,7 +40,7 @@ pub const DEPTH10_LEN: usize = 10;
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct OrderBookDepth10 {
diff --git a/nautilus_core/model/src/data/mod.rs b/nautilus_core/model/src/data/mod.rs
index 9ac16901fc1a..e4f256330aae 100644
--- a/nautilus_core/model/src/data/mod.rs
+++ b/nautilus_core/model/src/data/mod.rs
@@ -23,6 +23,8 @@ pub mod trade;
use nautilus_core::time::UnixNanos;
+use crate::ffi::data::deltas::OrderBookDeltas_API;
+
use self::{
bar::Bar, delta::OrderBookDelta, deltas::OrderBookDeltas, depth::OrderBookDepth10,
quote::QuoteTick, trade::TradeTick,
@@ -30,10 +32,10 @@ use self::{
#[repr(C)]
#[derive(Clone, Debug)]
-#[cfg_attr(feature = "trivial_copy", derive(Copy))]
#[allow(clippy::large_enum_variant)] // TODO: Optimize this (largest variant 1008 vs 136 bytes)
pub enum Data {
Delta(OrderBookDelta),
+ Deltas(OrderBookDeltas_API),
Depth10(OrderBookDepth10),
Quote(QuoteTick),
Trade(TradeTick),
@@ -48,6 +50,7 @@ impl HasTsInit for Data {
fn get_ts_init(&self) -> UnixNanos {
match self {
Data::Delta(d) => d.ts_init,
+ Data::Deltas(d) => d.ts_init,
Data::Depth10(d) => d.ts_init,
Data::Quote(q) => q.ts_init,
Data::Trade(t) => t.ts_init,
@@ -62,13 +65,13 @@ impl HasTsInit for OrderBookDelta {
}
}
-impl HasTsInit for OrderBookDepth10 {
+impl HasTsInit for OrderBookDeltas {
fn get_ts_init(&self) -> UnixNanos {
self.ts_init
}
}
-impl HasTsInit for OrderBookDeltas {
+impl HasTsInit for OrderBookDepth10 {
fn get_ts_init(&self) -> UnixNanos {
self.ts_init
}
@@ -103,6 +106,12 @@ impl From for Data {
}
}
+impl From for Data {
+ fn from(value: OrderBookDeltas_API) -> Self {
+ Self::Deltas(value)
+ }
+}
+
impl From for Data {
fn from(value: OrderBookDepth10) -> Self {
Self::Depth10(value)
@@ -129,5 +138,5 @@ impl From for Data {
#[no_mangle]
pub extern "C" fn data_clone(data: &Data) -> Data {
- *data // Actually a copy
+ data.clone()
}
diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs
index b2fa5b313b45..dc053f7f9f9d 100644
--- a/nautilus_core/model/src/data/order.rs
+++ b/nautilus_core/model/src/data/order.rs
@@ -19,7 +19,6 @@ use std::{
};
use nautilus_core::serialization::Serializable;
-use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use super::{quote::QuoteTick, trade::TradeTick};
@@ -49,7 +48,7 @@ pub const NULL_ORDER: BookOrder = BookOrder {
#[derive(Clone, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct BookOrder {
diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs
index 5c6a6ec29364..56067bc78d0c 100644
--- a/nautilus_core/model/src/data/quote.rs
+++ b/nautilus_core/model/src/data/quote.rs
@@ -42,7 +42,7 @@ use crate::{
#[serde(tag = "type")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct QuoteTick {
diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs
index 504bdd62333c..0ca757f047ac 100644
--- a/nautilus_core/model/src/data/trade.rs
+++ b/nautilus_core/model/src/data/trade.rs
@@ -37,7 +37,7 @@ use crate::{
#[serde(tag = "type")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
)]
#[cfg_attr(feature = "trivial_copy", derive(Copy))]
pub struct TradeTick {
@@ -202,9 +202,9 @@ mod tests {
#[rstest]
fn test_to_string(stub_trade_tick_ethusdt_buyer: TradeTick) {
- let tick = stub_trade_tick_ethusdt_buyer;
+ let trade = stub_trade_tick_ethusdt_buyer;
assert_eq!(
- tick.to_string(),
+ trade.to_string(),
"ETHUSDT-PERP.BINANCE,10000.0000,1.00000000,BUYER,123456789,0"
);
}
@@ -222,36 +222,36 @@ mod tests {
"ts_init": 1
}"#;
- let tick: TradeTick = serde_json::from_str(raw_string).unwrap();
+ let trade: TradeTick = serde_json::from_str(raw_string).unwrap();
- assert_eq!(tick.aggressor_side, AggressorSide::Buyer);
+ assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
}
#[rstest]
fn test_from_pyobject(stub_trade_tick_ethusdt_buyer: TradeTick) {
pyo3::prepare_freethreaded_python();
- let tick = stub_trade_tick_ethusdt_buyer;
+ let trade = stub_trade_tick_ethusdt_buyer;
Python::with_gil(|py| {
- let tick_pyobject = tick.into_py(py);
+ let tick_pyobject = trade.into_py(py);
let parsed_tick = TradeTick::from_pyobject(tick_pyobject.as_ref(py)).unwrap();
- assert_eq!(parsed_tick, tick);
+ assert_eq!(parsed_tick, trade);
});
}
#[rstest]
fn test_json_serialization(stub_trade_tick_ethusdt_buyer: TradeTick) {
- let tick = stub_trade_tick_ethusdt_buyer;
- let serialized = tick.as_json_bytes().unwrap();
+ let trade = stub_trade_tick_ethusdt_buyer;
+ let serialized = trade.as_json_bytes().unwrap();
let deserialized = TradeTick::from_json_bytes(serialized).unwrap();
- assert_eq!(deserialized, tick);
+ assert_eq!(deserialized, trade);
}
#[rstest]
fn test_msgpack_serialization(stub_trade_tick_ethusdt_buyer: TradeTick) {
- let tick = stub_trade_tick_ethusdt_buyer;
- let serialized = tick.as_msgpack_bytes().unwrap();
+ let trade = stub_trade_tick_ethusdt_buyer;
+ let serialized = trade.as_msgpack_bytes().unwrap();
let deserialized = TradeTick::from_msgpack_bytes(serialized).unwrap();
- assert_eq!(deserialized, tick);
+ assert_eq!(deserialized, trade);
}
}
diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs
index 6ffafc2fc416..5aec5ddca4cb 100644
--- a/nautilus_core/model/src/enums.rs
+++ b/nautilus_core/model/src/enums.rs
@@ -21,7 +21,7 @@ use pyo3::{exceptions::PyValueError, prelude::*, types::PyType, PyTypeInfo};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr};
-use crate::{enum_for_python, enum_strum_serde, python::EnumIterator};
+use crate::{enum_for_python, enum_strum_serde, python::common::EnumIterator};
pub trait FromU8 {
fn from_u8(value: u8) -> Option
@@ -50,7 +50,7 @@ pub trait FromU8 {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum AccountType {
/// An account with unleveraged cash assets only.
@@ -85,7 +85,7 @@ pub enum AccountType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum AggregationSource {
/// The data is externally aggregated (outside the Nautilus system boundary).
@@ -117,7 +117,7 @@ pub enum AggregationSource {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum AggressorSide {
/// There was no specific aggressor for the trade.
@@ -162,7 +162,7 @@ impl FromU8 for AggressorSide {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
#[allow(non_camel_case_types)]
pub enum AssetClass {
@@ -210,7 +210,7 @@ pub enum AssetClass {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum InstrumentClass {
/// A spot market instrument class. The current market price of an instrument that is bought or sold for immediate delivery and payment.
@@ -263,7 +263,7 @@ pub enum InstrumentClass {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum BarAggregation {
/// Based on a number of ticks.
@@ -337,7 +337,7 @@ pub enum BarAggregation {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum BookAction {
/// An order is added to the book.
@@ -388,7 +388,7 @@ impl FromU8 for BookAction {
#[allow(non_camel_case_types)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum BookType {
/// Top-of-book best bid/ask, one level per side.
@@ -433,7 +433,7 @@ impl FromU8 for BookType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum ContingencyType {
/// Not a contingent order.
@@ -470,7 +470,7 @@ pub enum ContingencyType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum CurrencyType {
/// A type of cryptocurrency or crypto token.
@@ -505,7 +505,7 @@ pub enum CurrencyType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum InstrumentCloseType {
/// When the market session ended.
@@ -537,7 +537,7 @@ pub enum InstrumentCloseType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
#[allow(clippy::enum_variant_names)]
pub enum LiquiditySide {
@@ -573,7 +573,7 @@ pub enum LiquiditySide {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum MarketStatus {
/// The market session is in the pre-open.
@@ -620,7 +620,7 @@ pub enum MarketStatus {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum HaltReason {
/// The venue or market session is not halted.
@@ -655,7 +655,7 @@ pub enum HaltReason {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum OmsType {
/// There is no specific type of order management specified (will defer to the venue).
@@ -691,7 +691,7 @@ pub enum OmsType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum OptionKind {
/// A Call option gives the holder the right, but not the obligation, to buy an underlying asset at a specified strike price within a specified period of time.
@@ -724,7 +724,7 @@ pub enum OptionKind {
#[allow(clippy::enum_variant_names)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum OrderSide {
/// No order side is specified.
@@ -789,7 +789,7 @@ impl FromU8 for OrderSide {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum OrderStatus {
/// The order is initialized (instantiated) within the Nautilus system.
@@ -857,7 +857,7 @@ pub enum OrderStatus {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum OrderType {
/// A market order to buy or sell at the best available price in the current market.
@@ -911,7 +911,7 @@ pub enum OrderType {
#[allow(clippy::enum_variant_names)]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum PositionSide {
/// No position side is specified (only valid in the context of a filter for actions involving positions).
@@ -948,7 +948,7 @@ pub enum PositionSide {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum PriceType {
/// A quoted order price where a buyer is willing to buy a quantity of an instrument.
@@ -986,7 +986,7 @@ pub enum PriceType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum TimeInForce {
/// Good Till Canceled (GTC) - the order remains active until canceled.
@@ -1033,7 +1033,7 @@ pub enum TimeInForce {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum TradingState {
/// Normal trading operations.
@@ -1068,7 +1068,7 @@ pub enum TradingState {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum TrailingOffsetType {
/// No trailing offset type is specified (invalid for trailing type orders).
@@ -1108,7 +1108,7 @@ pub enum TrailingOffsetType {
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
- pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
+ pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums")
)]
pub enum TriggerType {
/// No trigger type is specified (invalid for orders with a trigger).
diff --git a/nautilus_core/model/src/ffi/data/deltas.rs b/nautilus_core/model/src/ffi/data/deltas.rs
new file mode 100644
index 000000000000..99a80d255932
--- /dev/null
+++ b/nautilus_core/model/src/ffi/data/deltas.rs
@@ -0,0 +1,125 @@
+// -------------------------------------------------------------------------------------------------
+// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
+// https://nautechsystems.io
+//
+// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// -------------------------------------------------------------------------------------------------
+
+use std::ops::{Deref, DerefMut};
+
+use nautilus_core::{ffi::cvec::CVec, time::UnixNanos};
+
+use crate::{
+ data::{delta::OrderBookDelta, deltas::OrderBookDeltas},
+ enums::BookAction,
+ identifiers::instrument_id::InstrumentId,
+};
+
+/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`OrderBookDeltas`].
+///
+/// This struct wraps `OrderBookDeltas` in a way that makes it compatible with C function
+/// calls, enabling interaction with `OrderBookDeltas` in a C environment.
+///
+/// It implements the `Deref` trait, allowing instances of `OrderBookDeltas_API` to be
+/// dereferenced to `OrderBookDeltas`, providing access to `OrderBookDeltas`'s methods without
+/// having to manually access the underlying `OrderBookDeltas` instance.
+#[repr(C)]
+#[derive(Debug, Clone)]
+#[allow(non_camel_case_types)]
+pub struct OrderBookDeltas_API(Box