Skip to content

Commit 4e9db3e

Browse files
committed
Migrate refactor of catalog to rust
1 parent 23900ec commit 4e9db3e

File tree

29 files changed

+4236
-1114
lines changed

29 files changed

+4236
-1114
lines changed

.docker/DockerfileUbuntu

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Dockerfile to reproduce locally an environment similar to what is run on the github runner
2+
#
3+
# From nautilus project's root folder:
4+
#
5+
# Build the image:
6+
# docker build -f .docker/DockerfileUbuntu -t nautilus-dev .
7+
#
8+
# Run interactively with local directory mounted:
9+
# docker run --rm -itv "$(pwd)":/workspace nautilus-dev bash
10+
#
11+
# Or run the default entrypoint:
12+
# docker run --rm -itv "$(pwd)":/workspace nautilus-dev
13+
#
14+
# Remove the image
15+
# docker image rm nautilus-dev
16+
17+
FROM ubuntu:22.04
18+
19+
# Set environment variables
20+
ENV DEBIAN_FRONTEND=noninteractive
21+
ENV BUILD_MODE=release
22+
ENV RUST_BACKTRACE=1
23+
ENV CARGO_INCREMENTAL=1
24+
ENV CC="clang"
25+
ENV CXX="clang++"
26+
27+
# Install system dependencies
28+
RUN apt-get update && apt-get install -y \
29+
curl \
30+
clang \
31+
git \
32+
pkg-config \
33+
make \
34+
capnproto \
35+
libcapnp-dev \
36+
gcc-aarch64-linux-gnu \
37+
&& rm -rf /var/lib/apt/lists/*
38+
39+
# Install Rust
40+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
41+
ENV PATH="/root/.cargo/bin:${PATH}"
42+
43+
# Install mold linker
44+
RUN curl -L https://github.com/rui314/mold/releases/download/v2.35.1/mold-2.35.1-x86_64-linux.tar.gz | tar -xz -C /usr/local --strip-components=1
45+
46+
# Install uv
47+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
48+
ENV PATH="/root/.cargo/bin:/root/.local/bin:${PATH}"
49+
50+
# Install Python 3.13
51+
RUN uv python install
52+
53+
# Set working directory
54+
WORKDIR /workspace
55+
56+
# Copy only necessary files for dependency setup
57+
# The actual source code will be mounted as a volume
58+
COPY ../scripts/rust-toolchain.sh scripts/
59+
COPY ../Cargo.toml Cargo.lock pyproject.toml rust-toolchain.toml ./
60+
61+
# Set up Rust toolchain based on project requirements
62+
RUN bash scripts/rust-toolchain.sh > /tmp/toolchain.txt && \
63+
TOOLCHAIN=$(cat /tmp/toolchain.txt) && \
64+
rustup toolchain install $TOOLCHAIN && \
65+
rustup default $TOOLCHAIN && \
66+
rustup component add clippy rustfmt
67+
68+
# Copy and set up entrypoint script for interactive development
69+
COPY .docker/entrypoint.sh /entrypoint.sh
70+
RUN chmod +x /entrypoint.sh
71+
72+
ENTRYPOINT ["/entrypoint.sh"]

.docker/entrypoint.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
# entrypoint script for DockerfileUbuntu
3+
4+
echo "=== Nautilus Trader Development Environment ==="
5+
echo "Rust version: $(rustc --version)"
6+
echo "UV version: $(uv --version)"
7+
echo "Working directory: $(pwd)"
8+
echo
9+
10+
echo "=== Setting PyO3 environment ==="
11+
export PYO3_PYTHON=/workspace/.venv/bin/python3
12+
echo "PYO3_PYTHON: $PYO3_PYTHON"
13+
echo
14+
15+
echo "=== Development environment ready! ==="
16+
echo "You can now run for example:"
17+
echo " make install-debug # Install nautilus in debug mode"
18+
echo " make cargo-test # Test Rust code"
19+
echo " make pytest # Run Python tests"
20+
echo " uv run python -c \"import nautilus_trader.backtest.engine;\" # Run a Python instruction"
21+
echo
22+
23+
# If no command is provided, check if we have a TTY and start appropriate shell
24+
if [ $# -eq 0 ]; then
25+
if [ -t 0 ]; then
26+
echo "Starting interactive shell..."
27+
exec bash
28+
else
29+
echo "No TTY detected. Use docker run -it for interactive mode."
30+
echo "Container ready for commands. Example:"
31+
echo " docker run --rm -itv \"\$(pwd)\":/workspace nautilus-dev"
32+
fi
33+
else
34+
exec "$@"
35+
fi

Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,6 @@ url = { version = "2.5.4", default-features = false }
220220
urlencoding = "2.1.3"
221221
ustr = { version = "1.1.0", features = ["serde"] }
222222
uuid = { version = "1.17.0", features = ["v4", "serde"] }
223-
walkdir = "2.5.0"
224223
webpki-roots = "1.0.0"
225224

226225
# dev-dependencies

crates/adapters/tardis/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ rustdoc-args = ["--cfg", "docsrs"]
4040
[dependencies]
4141
nautilus-core = { workspace = true }
4242
nautilus-model = { workspace = true, features = ["python"] }
43+
nautilus-persistence = { workspace = true }
4344
nautilus-serialization = { workspace = true }
4445

4546
anyhow = { workspace = true }

crates/adapters/tardis/src/replay.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,11 @@ use nautilus_model::{
3232
},
3333
identifiers::InstrumentId,
3434
};
35-
use nautilus_serialization::{
36-
arrow::{
37-
bars_to_arrow_record_batch_bytes, book_deltas_to_arrow_record_batch_bytes,
38-
book_depth10_to_arrow_record_batch_bytes, quotes_to_arrow_record_batch_bytes,
39-
trades_to_arrow_record_batch_bytes,
40-
},
41-
parquet::write_batch_to_parquet,
35+
use nautilus_persistence::parquet::write_batch_to_parquet;
36+
use nautilus_serialization::arrow::{
37+
bars_to_arrow_record_batch_bytes, book_deltas_to_arrow_record_batch_bytes,
38+
book_depth10_to_arrow_record_batch_bytes, quotes_to_arrow_record_batch_bytes,
39+
trades_to_arrow_record_batch_bytes,
4240
};
4341
use thousands::Separable;
4442
use ustr::Ustr;
@@ -431,7 +429,16 @@ fn batch_and_write_bars(bars: Vec<Bar>, bar_type: &BarType, date: NaiveDate, pat
431429
};
432430

433431
let filepath = path.join(parquet_filepath_bars(bar_type, date));
434-
match write_batch_to_parquet(batch, &filepath, None, None, None) {
432+
let filepath_str = filepath.to_string_lossy();
433+
434+
let rt = tokio::runtime::Runtime::new().unwrap();
435+
match rt.block_on(write_batch_to_parquet(
436+
batch,
437+
&filepath_str,
438+
None,
439+
None,
440+
None,
441+
)) {
435442
Ok(()) => tracing::info!("File written: {filepath:?}"),
436443
Err(e) => tracing::error!("Error writing {filepath:?}: {e:?}"),
437444
}
@@ -464,7 +471,16 @@ fn write_batch(
464471
path: &Path,
465472
) {
466473
let filepath = path.join(parquet_filepath(typename, instrument_id, date));
467-
match write_batch_to_parquet(batch, &filepath, None, None, None) {
474+
let filepath_str = filepath.to_string_lossy();
475+
476+
let rt = tokio::runtime::Runtime::new().unwrap();
477+
match rt.block_on(write_batch_to_parquet(
478+
batch,
479+
&filepath_str,
480+
None,
481+
None,
482+
None,
483+
)) {
468484
Ok(()) => tracing::info!("File written: {filepath:?}"),
469485
Err(e) => tracing::error!("Error writing {filepath:?}: {e:?}"),
470486
}

crates/core/src/datetime.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,55 @@ pub fn unix_nanos_to_iso8601(unix_nanos: UnixNanos) -> String {
117117
datetime.to_rfc3339_opts(SecondsFormat::Nanos, true)
118118
}
119119

120+
/// Converts an ISO 8601 (RFC 3339) format string to UNIX nanoseconds timestamp.
121+
///
122+
/// This function accepts various ISO 8601 formats including:
123+
/// - Full RFC 3339 with nanosecond precision: "2024-02-10T14:58:43.456789Z"
124+
/// - RFC 3339 without fractional seconds: "2024-02-10T14:58:43Z"
125+
/// - Simple date format: "2024-02-10" (interpreted as midnight UTC)
126+
///
127+
/// # Parameters
128+
///
129+
/// - `date_string`: The ISO 8601 formatted date string to parse
130+
///
131+
/// # Returns
132+
///
133+
/// Returns `Ok(UnixNanos)` if the string is successfully parsed, or an error if the format
134+
/// is invalid or the timestamp is out of range.
135+
///
136+
/// # Errors
137+
///
138+
/// Returns an error if:
139+
/// - The string format is not a valid ISO 8601 format
140+
/// - The timestamp is out of range for `UnixNanos`
141+
/// - The date/time values are invalid
142+
///
143+
/// # Examples
144+
///
145+
/// ```rust
146+
/// use nautilus_core::datetime::iso8601_to_unix_nanos;
147+
/// use nautilus_core::UnixNanos;
148+
///
149+
/// // Full RFC 3339 format
150+
/// let nanos = iso8601_to_unix_nanos("2024-02-10T14:58:43.456789Z".to_string())?;
151+
/// assert_eq!(nanos, UnixNanos::from(1_707_577_123_456_789_000));
152+
///
153+
/// // Without fractional seconds
154+
/// let nanos = iso8601_to_unix_nanos("2024-02-10T14:58:43Z".to_string())?;
155+
/// assert_eq!(nanos, UnixNanos::from(1_707_577_123_000_000_000));
156+
///
157+
/// // Simple date format (midnight UTC)
158+
/// let nanos = iso8601_to_unix_nanos("2024-02-10".to_string())?;
159+
/// assert_eq!(nanos, UnixNanos::from(1_707_523_200_000_000_000));
160+
/// # Ok::<(), anyhow::Error>(())
161+
/// ```
162+
#[inline]
163+
pub fn iso8601_to_unix_nanos(date_string: String) -> anyhow::Result<UnixNanos> {
164+
date_string
165+
.parse::<UnixNanos>()
166+
.map_err(|e| anyhow::anyhow!("Failed to parse ISO 8601 string '{}': {}", date_string, e))
167+
}
168+
120169
/// Converts a UNIX nanoseconds timestamp to an ISO 8601 (RFC 3339) format string
121170
/// with millisecond precision.
122171
#[inline]
@@ -492,4 +541,36 @@ mod tests {
492541
let result = is_leap_year(year);
493542
assert_eq!(result, expected);
494543
}
544+
545+
#[rstest]
546+
#[case("1970-01-01T00:00:00.000000000Z", 0)] // Unix epoch
547+
#[case("1970-01-01T00:00:00.000000001Z", 1)] // 1 nanosecond
548+
#[case("1970-01-01T00:00:00.001000000Z", 1_000_000)] // 1 millisecond
549+
#[case("1970-01-01T00:00:01.000000000Z", 1_000_000_000)] // 1 second
550+
#[case("2023-12-18T00:00:00.000000000Z", 1_702_857_600_000_000_000)] // Specific date
551+
#[case("2024-02-10T14:58:43.456789Z", 1_707_577_123_456_789_000)] // RFC3339 with fractions
552+
#[case("2024-02-10T14:58:43Z", 1_707_577_123_000_000_000)] // RFC3339 without fractions
553+
#[case("2024-02-10", 1_707_523_200_000_000_000)] // Simple date format
554+
fn test_iso8601_to_unix_nanos(#[case] input: &str, #[case] expected: u64) {
555+
let result = iso8601_to_unix_nanos(input.to_string()).unwrap();
556+
assert_eq!(result.as_u64(), expected);
557+
}
558+
559+
#[rstest]
560+
#[case("invalid-date")] // Invalid format
561+
#[case("2024-02-30")] // Invalid date
562+
#[case("2024-13-01")] // Invalid month
563+
#[case("not a timestamp")] // Random string
564+
fn test_iso8601_to_unix_nanos_invalid(#[case] input: &str) {
565+
let result = iso8601_to_unix_nanos(input.to_string());
566+
assert!(result.is_err());
567+
}
568+
569+
#[rstest]
570+
fn test_iso8601_roundtrip() {
571+
let original_nanos = UnixNanos::from(1_707_577_123_456_789_000);
572+
let iso8601_string = unix_nanos_to_iso8601(original_nanos);
573+
let parsed_nanos = iso8601_to_unix_nanos(iso8601_string).unwrap();
574+
assert_eq!(parsed_nanos, original_nanos);
575+
}
495576
}

crates/persistence/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ nautilus-model = { workspace = true, features = ["stubs"] }
5252
nautilus-serialization = { workspace = true, features = ["python"] }
5353

5454
anyhow = { workspace = true }
55+
arrow = { workspace = true }
5556
binary-heap-plus = { workspace = true }
5657
compare = { workspace = true }
58+
chrono = { workspace = true }
5759
datafusion = { workspace = true }
5860
futures = { workspace = true }
5961
heck = { workspace = true }
@@ -65,7 +67,8 @@ pyo3 = { workspace = true, optional = true }
6567
serde = { workspace = true }
6668
serde_json = { workspace = true }
6769
tokio = { workspace = true }
68-
walkdir = { workspace = true }
70+
unbounded-interval-tree = { workspace = true }
71+
url = { workspace = true }
6972

7073
[dev-dependencies]
7174
nautilus-testkit = { workspace = true }

0 commit comments

Comments
 (0)