From 0d024a99dff48e02a3edead3223f264f301175f1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 07:27:09 +1100 Subject: [PATCH 01/71] Bump version --- .pre-commit-config.yaml | 4 +- RELEASES.md | 15 +++++++ nautilus_core/Cargo.lock | 44 ++++++++++----------- nautilus_core/Cargo.toml | 2 +- poetry.lock | 84 ++++++++++++++++++++-------------------- pyproject.toml | 6 +-- version.json | 2 +- 7 files changed, 86 insertions(+), 71 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4657e06d2d17..344d787d4b12 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.2.0 + rev: 24.3.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.3.2 + rev: v0.3.3 hooks: - id: ruff args: ["--fix"] diff --git a/RELEASES.md b/RELEASES.md index faad6f5c6084..f7aa2b08112d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,18 @@ +# NautilusTrader 1.190.0 Beta + +Released on TBD (UTC). + +### Enhancements +None + +### Breaking Changes +None + +### Fixes +None + +--- + # NautilusTrader 1.189.0 Beta Released on 15th March 2024 (UTC). diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 6c303e903d00..7c4d3f6ca5d8 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -785,9 +785,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", ] @@ -938,7 +938,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.2", + "clap 4.5.3", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1750,9 +1750,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1769,9 +1769,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", @@ -1966,7 +1966,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -1989,7 +1989,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.2", + "h2 0.4.3", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2409,7 +2409,7 @@ dependencies = [ [[package]] name = "nautilus-accounting" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2425,7 +2425,7 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "chrono", @@ -2457,7 +2457,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.19.0" +version = "0.20.0" dependencies = [ "cbindgen", "nautilus-common", @@ -2471,7 +2471,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2497,7 +2497,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2516,7 +2516,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "criterion", @@ -2540,7 +2540,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "nautilus-core", @@ -2552,7 +2552,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "nautilus-common", @@ -2567,7 +2567,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "cbindgen", @@ -2595,7 +2595,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "axum", @@ -2620,7 +2620,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.19.0" +version = "0.20.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -2644,7 +2644,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.19.0" +version = "0.20.0" dependencies = [ "nautilus-accounting", "nautilus-adapters", @@ -3565,7 +3565,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.25", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 19d23af3f414..f4250965af10 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -18,7 +18,7 @@ members = [ [workspace.package] rust-version = "1.76.0" -version = "0.19.0" +version = "0.20.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" diff --git a/poetry.lock b/poetry.lock index 41a79dedf09d..6cfa48fb7761 100644 --- a/poetry.lock +++ b/poetry.lock @@ -202,33 +202,33 @@ msgspec = ">=0.18.5" [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -1945,28 +1945,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "006f49cde91f1b880860b76d79ec687315f7c8fbf3e7d49624c8546a10dd1b19" +content-hash = "d43b785d4f415dbe6d3f380828d8449af8cd81c8a88535d83a98d03576048c80" diff --git a/pyproject.toml b/pyproject.toml index fb6a56ae2082..c1bd1594fba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.189.0" +version = "1.190.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -78,12 +78,12 @@ ib = ["nautilus_ibapi", "async-timeout", "defusedxml"] optional = true [tool.poetry.group.dev.dependencies] -black = "^24.2.0" +black = "^24.3.0" docformatter = "^1.7.5" mypy = "^1.9.0" pandas-stubs = "^2.1.4" pre-commit = "^3.6.2" -ruff = "^0.3.2" +ruff = "^0.3.3" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" diff --git a/version.json b/version.json index 9ffa1cf78a3e..619e83a7a44c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.189.0", + "message": "v1.190.0", "color": "orange" } From 337d25d75450f45edba96a962f2bed768e9533f0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 07:48:32 +1100 Subject: [PATCH 02/71] Update sandbox for MBO replay testing --- .../adapters/src/databento/bin/sandbox.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/nautilus_core/adapters/src/databento/bin/sandbox.rs b/nautilus_core/adapters/src/databento/bin/sandbox.rs index b84d37edc3b6..b70f7f96b1f6 100644 --- a/nautilus_core/adapters/src/databento/bin/sandbox.rs +++ b/nautilus_core/adapters/src/databento/bin/sandbox.rs @@ -5,7 +5,8 @@ use databento::{ live::Subscription, LiveClient, }; -use dbn::TradeMsg; +use dbn::{MboMsg, TradeMsg}; +use time::OffsetDateTime; #[tokio::main] async fn main() { @@ -20,9 +21,10 @@ async fn main() { client .subscribe( &Subscription::builder() - .schema(Schema::Trades) + .schema(Schema::Mbo) .stype_in(SType::RawSymbol) .symbols("ESM4") + .start(OffsetDateTime::from_unix_timestamp_nanos(0).unwrap()) .build(), ) .await @@ -30,9 +32,18 @@ async fn main() { client.start().await.unwrap(); + let mut count = 0; + while let Some(record) = client.next_record().await.unwrap() { - if let Some(trade) = record.get::() { - println!("{trade:#?}"); + if let Some(msg) = record.get::() { + println!("{msg:#?}"); + } + if let Some(msg) = record.get::() { + println!( + "Received delta: {} {} flags={}", + count, msg.hd.ts_event, msg.flags, + ); + count += 1; } } } From 62f8c863845f378e9bb527d083c8830524203d99 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 09:16:55 +1100 Subject: [PATCH 03/71] Update time dependency --- nautilus_core/adapters/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 6d476d209e17..519cb242a0ee 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -38,7 +38,7 @@ ustr = { workspace = true } databento = { version = "0.7.1", optional = true } dbn = { version = "0.16.0", optional = true, features = ["python"] } streaming-iterator = "0.1.9" -time = "0.3.31" +time = "0.3.34" [dev-dependencies] criterion = { workspace = true } From 81d8178b13755454ff3eaead2be7dee853ee914e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 09:22:44 +1100 Subject: [PATCH 04/71] Improve Redis error handling and logging --- RELEASES.md | 6 +- .../binance_futures_testnet_market_maker.py | 6 +- nautilus_core/Cargo.lock | 121 +++++------------- nautilus_core/Cargo.toml | 2 +- nautilus_core/common/src/ffi/msgbus.rs | 10 +- nautilus_core/common/src/msgbus.rs | 17 +-- nautilus_core/common/src/redis.rs | 33 +++-- nautilus_core/core/Cargo.toml | 2 +- nautilus_core/infrastructure/Cargo.toml | 1 + nautilus_core/infrastructure/src/redis.rs | 9 +- nautilus_trader/common/config.py | 3 + 11 files changed, 87 insertions(+), 123 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f7aa2b08112d..f0b002458798 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,7 +3,9 @@ Released on TBD (UTC). ### Enhancements -None +- Improved Redis cache adapter and message bus error handling and logging +- Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection +- Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies ### Breaking Changes None @@ -20,7 +22,7 @@ Released on 15th March 2024 (UTC). ### Enhancements - Implemented Binance order book snapshot rebuilds on websocket reconnect (see integration guide) - Added additional validations for `OrderMatchingEngine` (will now raise a `RuntimeError` when a price or size precision for `OrderFilled` does not match the instruments precisions) -- Added `LoggingConfig.use_pyo3` option for pyo3 based logging initialization (worse performance but allows visibility into logs originating from Rust) +- Added `LoggingConfig.use_pyo3` config option for pyo3 based logging initialization (worse performance but allows visibility into logs originating from Rust) - Added `exchange` field to `FuturesContract`, `FuturesSpread`, `OptionsContract` and `OptionsSpread` (optional) ### Breaking Changes diff --git a/examples/live/binance/binance_futures_testnet_market_maker.py b/examples/live/binance/binance_futures_testnet_market_maker.py index f2b7c4c1664c..2c42cd766b86 100644 --- a/examples/live/binance/binance_futures_testnet_market_maker.py +++ b/examples/live/binance/binance_futures_testnet_market_maker.py @@ -46,7 +46,7 @@ # log_level_file="DEBUG", # log_file_format="json", log_colors=True, - use_pyo3=False, + use_pyo3=True, ), exec_engine=LiveExecEngineConfig( reconciliation=True, @@ -54,12 +54,12 @@ filter_position_reports=True, ), cache=CacheConfig( - # database=DatabaseConfig(), + # database=DatabaseConfig(timeout=2), timestamps_as_iso8601=True, flush_on_start=False, ), # message_bus=MessageBusConfig( - # database=DatabaseConfig(), + # database=DatabaseConfig(timeout=2), # encoding="json", # timestamps_as_iso8601=True, # streams_prefix="quoters", diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 7c4d3f6ca5d8..a013b415e128 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -671,7 +671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49" dependencies = [ "clap 3.2.25", - "heck", + "heck 0.4.1", "indexmap 1.9.3", "log", "proc-macro2", @@ -1834,6 +1834,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1973,7 +1979,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", "tower-service", "tracing", @@ -2025,7 +2031,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.6", + "socket2", "tokio", ] @@ -2503,7 +2509,7 @@ dependencies = [ "cbindgen", "chrono", "criterion", - "heck", + "heck 0.5.0", "iai", "pyo3", "rmp-serde", @@ -2563,6 +2569,7 @@ dependencies = [ "rmp-serde", "rstest", "serde_json", + "tracing", ] [[package]] @@ -3363,7 +3370,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "pyo3-build-config", "quote", @@ -3459,9 +3466,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.24.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +checksum = "14c442de91f2a085154b1e1b374d5d5edf5bc49d2ebbfdf47e67edd6c2df568d" dependencies = [ "arc-swap", "async-trait", @@ -3472,16 +3479,16 @@ dependencies = [ "itoa", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", - "rustls-native-certs 0.6.3", - "rustls-pemfile 1.0.4", - "rustls-webpki 0.101.7", + "rustls", + "rustls-native-certs", + "rustls-pemfile 2.1.1", + "rustls-pki-types", "ryu", "sha1_smol", - "socket2 0.4.10", + "socket2", "tokio", "tokio-retry", - "tokio-rustls 0.24.1", + "tokio-rustls", "tokio-util", "url", ] @@ -3765,18 +3772,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.22.2" @@ -3786,23 +3781,11 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - [[package]] name = "rustls-native-certs" version = "0.7.0" @@ -3841,16 +3824,6 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustls-webpki" version = "0.102.2" @@ -3898,16 +3871,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" @@ -4101,7 +4064,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -4113,16 +4076,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.6" @@ -4263,7 +4216,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -4434,7 +4387,7 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -4447,7 +4400,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -4553,7 +4506,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro-error", "proc-macro2", "quote", @@ -4725,7 +4678,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -4762,23 +4715,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.10", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls", "rustls-pki-types", "tokio", ] @@ -4801,12 +4744,12 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.22.2", - "rustls-native-certs 0.7.0", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls 0.25.0", + "tokio-rustls", "tungstenite", "webpki-roots", ] @@ -4984,7 +4927,7 @@ dependencies = [ "log", "native-tls", "rand", - "rustls 0.22.2", + "rustls", "rustls-pki-types", "sha1", "thiserror", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index f4250965af10..01fe3974a944 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -35,7 +35,7 @@ log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_ pyo3 = { version = "0.20.3", features = ["anyhow", "rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -redis = { version = "0.24.0", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } +redis = { version = "0.25.1", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } rmp-serde = "1.1.2" rust_decimal = "1.34.3" rust_decimal_macros = "1.34.2" diff --git a/nautilus_core/common/src/ffi/msgbus.rs b/nautilus_core/common/src/ffi/msgbus.rs index 1011cde514f2..477e90309dc0 100644 --- a/nautilus_core/common/src/ffi/msgbus.rs +++ b/nautilus_core/common/src/ffi/msgbus.rs @@ -80,12 +80,10 @@ pub unsafe extern "C" fn msgbus_new( let name = optional_cstr_to_str(name_ptr).map(|s| s.to_string()); let instance_id = UUID4::from(cstr_to_str(instance_id_ptr)); let config = optional_bytes_to_json(config_ptr); - MessageBus_API(Box::new(MessageBus::new( - trader_id, - instance_id, - name, - config, - ))) + MessageBus_API(Box::new( + MessageBus::new(trader_id, instance_id, name, config) + .expect("Error initializing `MessageBus`"), + )) } #[no_mangle] diff --git a/nautilus_core/common/src/msgbus.rs b/nautilus_core/common/src/msgbus.rs index 3651d1707087..c15e90209839 100644 --- a/nautilus_core/common/src/msgbus.rs +++ b/nautilus_core/common/src/msgbus.rs @@ -168,13 +168,12 @@ pub struct MessageBus { impl MessageBus { /// Initializes a new instance of the [`MessageBus`]. - #[must_use] pub fn new( trader_id: TraderId, instance_id: UUID4, name: Option, config: Option>, - ) -> Self { + ) -> anyhow::Result { let config = config.unwrap_or_default(); let has_backing = config .get("database") @@ -183,16 +182,14 @@ impl MessageBus { let (tx, rx) = channel::(); let _join_handler = thread::Builder::new() .name("msgbus".to_string()) - .spawn(move || { - Self::handle_messages(rx, trader_id, instance_id, config); - }) + .spawn(move || Self::handle_messages(rx, trader_id, instance_id, config)) .expect("Error spawning `msgbus` thread"); Some(tx) } else { None }; - Self { + Ok(Self { tx, trader_id, instance_id, @@ -206,7 +203,7 @@ impl MessageBus { endpoints: IndexMap::new(), correlation_index: IndexMap::new(), has_backing, - } + }) } /// Returns the registered endpoint addresses. @@ -412,7 +409,7 @@ impl MessageBus { trader_id: TraderId, instance_id: UUID4, config: HashMap, - ) { + ) -> anyhow::Result<()> { let database_config = config .get("database") .expect("No `MessageBusConfig` `database` config specified"); @@ -436,8 +433,8 @@ fn handle_messages_with_redis_if_enabled( trader_id: TraderId, instance_id: UUID4, config: HashMap, -) { - handle_messages_with_redis(rx, trader_id, instance_id, config); +) -> anyhow::Result<()> { + handle_messages_with_redis(rx, trader_id, instance_id, config) } /// Handles messages using a default method if the "redis" feature is not enabled. diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index e49ebacfbb56..c0c2f9f5fbad 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -24,6 +24,7 @@ use nautilus_core::{time::duration_since_unix_epoch, uuid::UUID4}; use nautilus_model::identifiers::trader_id::TraderId; use redis::*; use serde_json::{json, Value}; +use tracing::debug; use crate::msgbus::BusMessage; @@ -36,10 +37,15 @@ pub fn handle_messages_with_redis( trader_id: TraderId, instance_id: UUID4, config: HashMap, -) { +) -> anyhow::Result<()> { + debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); let redis_url = get_redis_url(&config); - let client = redis::Client::open(redis_url).unwrap(); - let mut conn = client.get_connection().unwrap(); + let default_timeout = 20; + let timeout = get_timeout_duration(&config, default_timeout); + let client = redis::Client::open(redis_url)?; + let mut conn = client.get_connection_with_timeout(timeout)?; + debug!("Connected"); + let stream_name = get_stream_name(trader_id, instance_id, &config); // Autotrimming @@ -68,7 +74,7 @@ pub fn handle_messages_with_redis( autotrim_duration, &mut last_trim_index, &mut buffer, - ); + )?; last_drain = Instant::now(); } else { // Continue to receive and handle messages until channel is hung up @@ -88,8 +94,10 @@ pub fn handle_messages_with_redis( autotrim_duration, &mut last_trim_index, &mut buffer, - ); + )?; } + + Ok(()) } fn drain_buffer( @@ -98,7 +106,7 @@ fn drain_buffer( autotrim_duration: Option, last_trim_index: &mut HashMap, buffer: &mut VecDeque, -) { +) -> anyhow::Result<()> { let mut pipe = redis::pipe(); pipe.atomic(); @@ -133,9 +141,7 @@ fn drain_buffer( } } - if let Err(e) = pipe.query::<()>(conn) { - eprintln!("{e}"); - } + pipe.query::<()>(conn).map_err(anyhow::Error::from) } pub fn get_redis_url(config: &HashMap) -> String { @@ -168,6 +174,15 @@ pub fn get_redis_url(config: &HashMap) -> String { ) } +pub fn get_timeout_duration(config: &HashMap, default: u64) -> Duration { + let timeout_seconds = config + .get("database") + .and_then(|database| database.get("timeout").and_then(|v| v.as_u64())) + .unwrap_or(default); + + Duration::from_secs(timeout_seconds) +} + pub fn get_buffer_interval(config: &HashMap) -> Duration { let buffer_interval_ms = config .get("buffer_interval_ms") diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index ffc67a0da24c..833ff7a6e23f 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -19,7 +19,7 @@ serde = { workspace = true } serde_json = { workspace = true } ustr = { workspace = true } uuid = { workspace = true } -heck = "0.4.1" +heck = "0.5.0" [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 886209f35292..7a50ffb45532 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -19,6 +19,7 @@ pyo3 = { workspace = true, optional = true } redis = { workspace = true, optional = true } rmp-serde = { workspace = true } serde_json = { workspace = true } +tracing = {workspace = true } [dev-dependencies] rstest = { workspace = true } diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 657e2c794880..91a174a2f59e 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -21,11 +21,12 @@ use std::{ }; use anyhow::{anyhow, bail, Result}; -use nautilus_common::redis::{get_buffer_interval, get_redis_url}; +use nautilus_common::redis::{get_buffer_interval, get_redis_url, get_timeout_duration}; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::{Commands, Connection, Pipeline}; use serde_json::json; +use tracing::debug; use crate::cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}; @@ -82,9 +83,13 @@ impl CacheDatabase for RedisCacheDatabase { instance_id: UUID4, config: HashMap, ) -> Result { + debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); let redis_url = get_redis_url(&config); + let default_timeout = 20; + let timeout = get_timeout_duration(&config, default_timeout); let client = redis::Client::open(redis_url)?; - let conn = client.get_connection().unwrap(); + let conn = client.get_connection_with_timeout(timeout)?; + debug!("Connected"); let (tx, rx) = channel::(); let trader_key = get_trader_key(trader_id, instance_id, &config); diff --git a/nautilus_trader/common/config.py b/nautilus_trader/common/config.py index 9afc90cb6503..ecb386b00cb0 100644 --- a/nautilus_trader/common/config.py +++ b/nautilus_trader/common/config.py @@ -249,6 +249,8 @@ class DatabaseConfig(NautilusConfig, frozen=True): The account password for the database connection. ssl : bool, default False If database should use an SSL enabled connection. + timeout : int, default 20 + The timeout (seconds) to wait for a new connection. Notes ----- @@ -262,6 +264,7 @@ class DatabaseConfig(NautilusConfig, frozen=True): username: str | None = None password: str | None = None ssl: bool = False + timeout: int | None = 20 class MessageBusConfig(NautilusConfig, frozen=True): From d17253cc2316651cbcd171c4df0fc331e9e5efa2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 09:52:32 +1100 Subject: [PATCH 05/71] Fix config tests --- tests/unit_tests/config/test_common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/config/test_common.py b/tests/unit_tests/config/test_common.py index 8522c3fb6e27..622e85080d3c 100644 --- a/tests/unit_tests/config/test_common.py +++ b/tests/unit_tests/config/test_common.py @@ -51,7 +51,7 @@ def test_equality_hash_repr() -> None: assert isinstance(hash(config1), int) assert ( repr(config1) - == "DatabaseConfig(type='redis', host=None, port=None, username=None, password=None, ssl=False)" + == "DatabaseConfig(type='redis', host=None, port=None, username=None, password=None, ssl=False, timeout=20)" ) @@ -60,7 +60,7 @@ def test_config_id() -> None: config = DatabaseConfig() # Act, Assert - assert config.id == "18a63bfe7acf0b0126940542dc4e261c58e326db70194e5c65949e26a2f5bf1b" + assert config.id == "c3fad60cbcd4eb9d9f19081f6f342f04a77f1328e9487f11696f9abc119ff0e1" def test_fully_qualified_name() -> None: @@ -83,6 +83,7 @@ def test_dict() -> None: "username": None, "password": None, "ssl": False, + "timeout": 20, } @@ -93,7 +94,7 @@ def test_json() -> None: # Act, Assert assert ( config.json() - == b'{"type":"redis","host":null,"port":null,"username":null,"password":null,"ssl":false}' + == b'{"type":"redis","host":null,"port":null,"username":null,"password":null,"ssl":false,"timeout":20}' ) From 6f0e1cbdb59740e7a84d3f2c1d85b2adfebe90c7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 10:58:52 +1100 Subject: [PATCH 06/71] Improve Binance ping_listen_key robustness --- .../adapters/binance/common/execution.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index 6682acfbcf90..3f8423a9b6f9 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -30,6 +30,7 @@ from nautilus_trader.adapters.binance.config import BinanceExecClientConfig from nautilus_trader.adapters.binance.http.account import BinanceAccountHttpAPI from nautilus_trader.adapters.binance.http.client import BinanceHttpClient +from nautilus_trader.adapters.binance.http.error import BinanceClientError from nautilus_trader.adapters.binance.http.error import BinanceError from nautilus_trader.adapters.binance.http.market import BinanceMarketHttpAPI from nautilus_trader.adapters.binance.http.user import BinanceUserDataHttpAPI @@ -283,19 +284,23 @@ async def _ping_listen_keys(self) -> None: while True: self._log.debug( f"Scheduled `ping_listen_keys` to run in " - f"{self._ping_listen_keys_interval}s.", + f"{self._ping_listen_keys_interval}s", ) await asyncio.sleep(self._ping_listen_keys_interval) if self._listen_key: - self._log.debug(f"Pinging WebSocket listen key {self._listen_key}...") - await self._http_user.keepalive_listen_key(listen_key=self._listen_key) + self._log.debug(f"Pinging WebSocket listen key {self._listen_key}") + try: + await self._http_user.keepalive_listen_key(listen_key=self._listen_key) + except BinanceClientError as ex: + # We may see this if an old listen key was used for the ping + self._log.error(f"Error pinging listen key: {ex}") except asyncio.CancelledError: - self._log.debug("Canceled `ping_listen_keys` task.") + self._log.debug("Canceled `ping_listen_keys` task") async def _disconnect(self) -> None: # Cancel tasks if self._ping_listen_keys_task: - self._log.debug("Canceling `ping_listen_keys` task...") + self._log.debug("Canceling `ping_listen_keys` task") self._ping_listen_keys_task.cancel() self._ping_listen_keys_task = None From cc824b9c421ef89f15cebc6e5a02aa0db445609e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 11:49:31 +1100 Subject: [PATCH 07/71] Fix JSON log file output --- RELEASES.md | 3 +- nautilus_core/Cargo.lock | 1 + nautilus_core/Cargo.toml | 2 +- nautilus_core/common/src/enums.rs | 28 ++++++++++++++----- nautilus_core/common/src/logging/logger.rs | 32 +++++++++++++++++----- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f0b002458798..12b9c31ffd4a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,6 +3,7 @@ Released on TBD (UTC). ### Enhancements +- Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging - Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection - Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies @@ -11,7 +12,7 @@ Released on TBD (UTC). None ### Fixes -None +- Fixed JSON format for log file output (was missing `timestamp` and `trader\_id`) --- diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index a013b415e128..95c5fdf28184 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2098,6 +2098,7 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 01fe3974a944..203a1e9b3a7f 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -28,7 +28,7 @@ documentation = "https://docs.nautilustrader.io" anyhow = "1.0.81" chrono = "0.4.35" futures = "0.3.30" -indexmap = "2.2.5" +indexmap = { version = "2.2.5", features = ["serde"] } itoa = "1.0.10" once_cell = "1.19.0" log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index 168569ca4d0e..a8fe533b4faa 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -203,28 +203,42 @@ pub enum LogLevel { )] pub enum LogColor { /// The default/normal log color. - #[strum(serialize = "")] + #[strum(serialize = "NORMAL")] Normal = 0, /// The green log color, typically used with [`LogLevel::Info`] log levels and associated with success events. - #[strum(serialize = "\x1b[92m")] + #[strum(serialize = "GREEN")] Green = 1, /// The blue log color, typically used with [`LogLevel::Info`] log levels and associated with user actions. - #[strum(serialize = "\x1b[94m")] + #[strum(serialize = "BLUE")] Blue = 2, /// The magenta log color, typically used with [`LogLevel::Info`] log levels. - #[strum(serialize = "\x1b[35m")] + #[strum(serialize = "MAGENTA")] Magenta = 3, /// The cyan log color, typically used with [`LogLevel::Info`] log levels. - #[strum(serialize = "\x1b[36m")] + #[strum(serialize = "CYAN")] Cyan = 4, /// The yellow log color, typically used with [`LogLevel::Warning`] log levels. - #[strum(serialize = "\x1b[1;33m")] + #[strum(serialize = "YELLOW")] Yellow = 5, /// The red log color, typically used with [`LogLevel::Error`] level. - #[strum(serialize = "\x1b[1;31m")] + #[strum(serialize = "RED")] Red = 6, } +impl LogColor { + pub fn as_ansi(&self) -> &str { + match *self { + Self::Normal => "", + Self::Green => "\x1b[92m", + Self::Blue => "\x1b[94m", + Self::Magenta => "\x1b[35m", + Self::Cyan => "\x1b[36m", + Self::Yellow => "\x1b[1;33m", + Self::Red => "\x1b[1;31m", + } + } +} + impl From for LogColor { fn from(value: u8) -> Self { match value { diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 96ec5b62294e..b6da47a59207 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -24,6 +24,7 @@ use std::{ thread, }; +use indexmap::IndexMap; use log::{ debug, error, info, kv::{ToValue, Value}, @@ -35,7 +36,7 @@ use nautilus_core::{ uuid::UUID4, }; use nautilus_model::identifiers::trader_id::TraderId; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use ustr::Ustr; use super::{LOGGING_BYPASSED, LOGGING_REALTIME}; @@ -175,6 +176,12 @@ pub struct LogLine { pub message: String, } +impl fmt::Display for LogLine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}: {}", self.level, self.component, self.message) + } +} + pub struct LogLineWrapper { line: LogLine, cache: Option, @@ -212,7 +219,7 @@ impl LogLineWrapper { format!( "\x1b[1m{}\x1b[0m {}[{}] {}.{}: {}\x1b[0m\n", self.timestamp, - &self.line.color.to_string(), + &self.line.color.as_ansi(), self.line.level, self.trader_id, &self.line.component, @@ -223,14 +230,25 @@ impl LogLineWrapper { pub fn get_json(&self) -> String { let json_string = - serde_json::to_string(&self.line).expect("Error serializing log event to string"); + serde_json::to_string(&self).expect("Error serializing log event to string"); format!("{json_string}\n") } } -impl fmt::Display for LogLine { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{}] {}: {}", self.level, self.component, self.message) +impl Serialize for LogLineWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut json_obj = IndexMap::new(); + json_obj.insert("timestamp".to_string(), self.timestamp.clone()); + json_obj.insert("trader_id".to_string(), self.trader_id.to_string()); + json_obj.insert("level".to_string(), self.line.level.to_string()); + json_obj.insert("color".to_string(), self.line.color.to_string()); + json_obj.insert("component".to_string(), self.line.component.to_string()); + json_obj.insert("message".to_string(), self.line.message.to_string()); + + json_obj.serialize(serializer) } } @@ -671,7 +689,7 @@ mod tests { assert_eq!( log_contents, - "{\"level\":\"INFO\",\"color\":\"Normal\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" + "{\"timestamp\":\"1970-01-20T02:20:00.000000000Z\",\"trader_id\":\"TRADER-001\",\"level\":\"INFO\",\"color\":\"NORMAL\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" ); } } From 26b541d405d690834a9784e769acbc13ebd2759c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 13:07:55 +1100 Subject: [PATCH 08/71] Fix ChandeMomentumOscillator divide by zero error --- RELEASES.md | 1 + nautilus_core/indicators/src/momentum/cmo.rs | 9 +++++++-- nautilus_trader/indicators/cmo.pyx | 8 ++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 12b9c31ffd4a..08299fa9d4b0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ None ### Fixes - Fixed JSON format for log file output (was missing `timestamp` and `trader\_id`) +- Fixed `ChandeMomentumOscillator` indicator divide by zero error --- diff --git a/nautilus_core/indicators/src/momentum/cmo.rs b/nautilus_core/indicators/src/momentum/cmo.rs index 263ddadc2b2a..b84a0713cbff 100644 --- a/nautilus_core/indicators/src/momentum/cmo.rs +++ b/nautilus_core/indicators/src/momentum/cmo.rs @@ -119,8 +119,13 @@ impl ChandeMomentumOscillator { 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()); + let divisor = self._average_gain.value() + self._average_loss.value(); + if divisor == 0.0 { + self.value = 0.0; + } else { + self.value = + 100.0 * (self._average_gain.value() - self._average_loss.value()) / divisor; + } } self._previous_close = close; } diff --git a/nautilus_trader/indicators/cmo.pyx b/nautilus_trader/indicators/cmo.pyx index 421f7d97331e..b60335050a2d 100644 --- a/nautilus_trader/indicators/cmo.pyx +++ b/nautilus_trader/indicators/cmo.pyx @@ -96,9 +96,13 @@ cdef class ChandeMomentumOscillator(Indicator): if self._average_gain.initialized and self._average_loss.initialized: self._set_initialized(True) + cdef double divisor if self.initialized: - self.value = 100.0 * (self._average_gain.value - self._average_loss.value) - self.value = self.value / (self._average_gain.value + self._average_loss.value) + divisor = self._average_gain.value + self._average_loss.value + if divisor == 0.0: + self.value = 0.0 + else: + self.value = 100.0 * (self._average_gain.value - self._average_loss.value) / divisor self._previous_close = close From b4aff4810fcb5388925cc70d5118d8f6e4695574 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 16:59:34 +1100 Subject: [PATCH 09/71] Derive Clone and Debug for orders --- nautilus_core/model/src/orders/base.rs | 16 ++++++ nautilus_core/model/src/orders/limit.rs | 1 + .../model/src/orders/limit_if_touched.rs | 1 + nautilus_core/model/src/orders/market.rs | 1 + .../model/src/orders/market_if_touched.rs | 1 + .../model/src/orders/market_to_limit.rs | 1 + nautilus_core/model/src/orders/stop_limit.rs | 1 + nautilus_core/model/src/orders/stop_market.rs | 1 + nautilus_core/model/src/orders/stubs.rs | 50 +++++++++++++++++-- .../model/src/orders/trailing_stop_limit.rs | 1 + .../model/src/orders/trailing_stop_market.rs | 1 + 11 files changed, 72 insertions(+), 3 deletions(-) diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 8926af432c64..314853d0e063 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -93,6 +93,7 @@ fn order_side_to_fixed(side: OrderSide) -> OrderSideFixed { } } +#[derive(Clone, Debug)] pub enum PassiveOrderType { Limit(LimitOrderType), Stop(StopOrderType), @@ -107,6 +108,7 @@ impl PartialEq for PassiveOrderType { } } +#[derive(Clone, Debug)] pub enum LimitOrderType { Limit(LimitOrder), MarketToLimit(MarketToLimitOrder), @@ -125,6 +127,7 @@ impl PartialEq for LimitOrderType { } } +#[derive(Clone, Debug)] pub enum StopOrderType { StopMarket(StopMarketOrder), StopLimit(StopLimitOrder), @@ -134,6 +137,19 @@ pub enum StopOrderType { TrailingStopLimit(TrailingStopLimitOrder), } +impl PartialEq for StopOrderType { + fn eq(&self, rhs: &Self) -> bool { + match self { + Self::StopMarket(o) => o.client_order_id == rhs.get_client_order_id(), + Self::StopLimit(o) => o.client_order_id == rhs.get_client_order_id(), + Self::MarketIfTouched(o) => o.client_order_id == rhs.get_client_order_id(), + Self::LimitIfTouched(o) => o.client_order_id == rhs.get_client_order_id(), + Self::TrailingStopMarket(o) => o.client_order_id == rhs.get_client_order_id(), + Self::TrailingStopLimit(o) => o.client_order_id == rhs.get_client_order_id(), + } + } +} + pub trait GetClientOrderId { fn get_client_order_id(&self) -> ClientOrderId; } diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 6e3ad588490c..fcbaebb9fdfd 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 710b4e9c1292..a638dea112f1 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 10a8045c605f..66fb95457999 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -42,6 +42,7 @@ use crate::{ }, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index c44ebd331cd2..537a0614da22 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index a5eb5baea726..a704cd8d6854 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 7c0e8060ef90..33dddb94a8ad 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index b6e9279990a4..47931c5cccc6 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/stubs.rs b/nautilus_core/model/src/orders/stubs.rs index 63996d02b2ad..0ce159bedc72 100644 --- a/nautilus_core/model/src/orders/stubs.rs +++ b/nautilus_core/model/src/orders/stubs.rs @@ -17,9 +17,9 @@ use std::str::FromStr; use nautilus_core::uuid::UUID4; -use super::limit::LimitOrder; +use super::{limit::LimitOrder, stop_market::StopMarketOrder}; use crate::{ - enums::{LiquiditySide, OrderSide, TimeInForce}, + enums::{LiquiditySide, OrderSide, TimeInForce, TriggerType}, events::order::filled::OrderFilled, identifiers::{ account_id::AccountId, @@ -149,7 +149,7 @@ impl TestOrderStubs { let trader = trader_id(); let strategy = strategy_id_ema_cross(); let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-20200814-102234-001-001-1")); + client_order_id.unwrap_or(ClientOrderId::from("O-19700101-010000-001-001-1")); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); LimitOrder::new( trader, @@ -179,4 +179,48 @@ impl TestOrderStubs { 12_321_312_321_312, ) } + + #[must_use] + pub fn stop_market_order( + instrument_id: InstrumentId, + order_side: OrderSide, + trigger_price: Price, + quantity: Quantity, + trigger_type: Option, + client_order_id: Option, + time_in_force: Option, + ) -> StopMarketOrder { + let trader = trader_id(); + let strategy = strategy_id_ema_cross(); + let client_order_id = + client_order_id.unwrap_or(ClientOrderId::from("O-19700101-010000-001-001-1")); + let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); + StopMarketOrder::new( + trader, + strategy, + instrument_id, + client_order_id, + order_side, + quantity, + trigger_price, + trigger_type.unwrap_or(TriggerType::BidAsk), + time_in_force, + None, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + UUID4::new(), + 12_321_312_321_312, + ) + } } diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index df58d146842b..b157b2dc3835 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -37,6 +37,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 539b96052f74..0fd025873aa1 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -38,6 +38,7 @@ use crate::{ types::{price::Price, quantity::Quantity}, }; +#[derive(Clone, Debug)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") From d50ee2f009840a223eafd07b39db47703a0ca512 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 17:00:31 +1100 Subject: [PATCH 10/71] Add OrderMatchingCore impl and tests --- nautilus_core/execution/src/matching_core.rs | 300 ++++++++++++++++++- 1 file changed, 289 insertions(+), 11 deletions(-) diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index eb2de41a1c5f..c502e5f1e387 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -41,9 +41,9 @@ pub struct OrderMatchingCore { pub last: Option, orders_bid: Vec, orders_ask: Vec, - trigger_stop_order: Option, - fill_market_order: Option, - fill_limit_order: Option, + trigger_stop_order: Option, + fill_market_order: Option, + fill_limit_order: Option, } impl OrderMatchingCore { @@ -51,9 +51,9 @@ impl OrderMatchingCore { pub fn new( instrument_id: InstrumentId, price_increment: Price, - trigger_stop_order: Option, - fill_market_order: Option, - fill_limit_order: Option, + trigger_stop_order: Option, + fill_market_order: Option, + fill_limit_order: Option, ) -> Self { Self { instrument_id, @@ -162,13 +162,17 @@ impl OrderMatchingCore { pub fn match_limit_order(&self, order: &LimitOrderType) { if self.is_limit_matched(order) { - // self.fill_limit_order.call(o) + if let Some(func) = self.fill_limit_order { + func(order.clone()); // TODO: Remove this clone (will need a lifetime) + } } } pub fn match_stop_order(&self, order: &StopOrderType) { if self.is_stop_matched(order) { - // self.fill_stop_order.call(o) + if let Some(func) = self.trigger_stop_order { + func(order.clone()); // TODO: Remove this clone (will need a lifetime) + } } } @@ -194,6 +198,8 @@ impl OrderMatchingCore { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use std::sync::Mutex; + use nautilus_model::{ enums::OrderSide, orders::stubs::TestOrderStubs, types::quantity::Quantity, }; @@ -201,6 +207,9 @@ mod tests { use super::*; + static TRIGGERED_STOPS: Mutex> = Mutex::new(Vec::new()); + static FILLED_LIMITS: Mutex> = Mutex::new(Vec::new()); + fn create_matching_core( instrument_id: InstrumentId, price_increment: Price, @@ -208,13 +217,132 @@ mod tests { OrderMatchingCore::new(instrument_id, price_increment, None, None, None) } + #[rstest] + fn test_add_order_bid_side() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Buy, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + + assert!(matching_core.get_orders_bid().contains(&passive_order)); + assert!(!matching_core.get_orders_ask().contains(&passive_order)); + assert_eq!(matching_core.get_orders_bid().len(), 1); + assert!(matching_core.get_orders_ask().is_empty()); + } + + #[rstest] + fn test_add_order_ask_side() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Sell, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + + assert!(matching_core.get_orders_ask().contains(&passive_order)); + assert!(!matching_core.get_orders_bid().contains(&passive_order)); + assert_eq!(matching_core.get_orders_ask().len(), 1); + assert!(matching_core.get_orders_bid().is_empty()); + } + + #[rstest] + fn test_reset() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Sell, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order).unwrap(); + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + matching_core.last = Some(Price::from("100.00")); + + matching_core.reset(); + + assert!(matching_core.bid.is_none()); + assert!(matching_core.ask.is_none()); + assert!(matching_core.last.is_none()); + assert!(matching_core.get_orders_bid().is_empty()); + assert!(matching_core.get_orders_ask().is_empty()); + } + + #[rstest] + fn test_delete_order_when_not_exists() { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + OrderSide::Buy, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + let result = matching_core.delete_order(&passive_order); + + assert!(result.is_err()); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_delete_order_when_exists(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + + let order = TestOrderStubs::limit_order( + instrument_id, + order_side, + Price::from("100.00"), + Quantity::from("100"), + None, + None, + ); + + let passive_order = PassiveOrderType::Limit(LimitOrderType::Limit(order)); + matching_core.add_order(passive_order.clone()).unwrap(); + matching_core.delete_order(&passive_order).unwrap(); + + assert!(matching_core.get_orders_ask().is_empty()); + assert!(matching_core.get_orders_bid().is_empty()); + } + #[rstest] #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)] #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)] #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("100.00"), + Price::from("100.00"), // Price below ask OrderSide::Buy, false )] @@ -228,14 +356,14 @@ mod tests { #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("102.00"), // <-- Price higher than ask (marketable) + Price::from("102.00"), // <-- Price above ask (marketable) OrderSide::Buy, true )] #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("101.00"), + Price::from("101.00"), // <-- Price above bid OrderSide::Sell, false )] @@ -278,4 +406,154 @@ mod tests { assert_eq!(result, expected); } + + #[rstest] + #[case(None, None, Price::from("100.00"), OrderSide::Buy, false)] + #[case(None, None, Price::from("100.00"), OrderSide::Sell, false)] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("102.00"), // Trigger above ask + OrderSide::Buy, + false + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("101.00"), // <-- Trigger at ask + OrderSide::Buy, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("100.00"), // <-- Trigger below ask + OrderSide::Buy, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("99.00"), // Trigger below bid + OrderSide::Sell, + false + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("100.00"), // <-- Trigger at bid + OrderSide::Sell, + true + )] + #[case( + Some(Price::from("100.00")), + Some(Price::from("101.00")), + Price::from("101.00"), // <-- Trigger above bid + OrderSide::Sell, + true + )] + fn test_is_stop_matched( + #[case] bid: Option, + #[case] ask: Option, + #[case] trigger_price: Price, + #[case] order_side: OrderSide, + #[case] expected: bool, + ) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let mut matching_core = create_matching_core(instrument_id, Price::from("0.01")); + matching_core.bid = bid; + matching_core.ask = ask; + + let order = TestOrderStubs::stop_market_order( + instrument_id, + order_side, + trigger_price, + Quantity::from("100"), + None, + None, + None, + ); + + let result = matching_core.is_stop_matched(&StopOrderType::StopMarket(order)); + + assert_eq!(result, expected); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_match_stop_order_when_triggered(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let trigger_price = Price::from("100.00"); + + fn trigger_stop_order_handler(order: StopOrderType) { + let order = order; + TRIGGERED_STOPS.lock().unwrap().push(order); + } + + let mut matching_core = OrderMatchingCore::new( + instrument_id, + Price::from("0.01"), + Some(trigger_stop_order_handler), + None, + None, + ); + + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + + let order = TestOrderStubs::stop_market_order( + instrument_id, + order_side, + trigger_price, + Quantity::from("100"), + None, + None, + None, + ); + + matching_core.match_stop_order(&StopOrderType::StopMarket(order.clone())); + + let triggered_stops = TRIGGERED_STOPS.lock().unwrap(); + assert_eq!(triggered_stops.len(), 1); + assert_eq!(triggered_stops[0], StopOrderType::StopMarket(order)); + } + + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_match_limit_order_when_triggered(#[case] order_side: OrderSide) { + let instrument_id = InstrumentId::from("AAPL.XNAS"); + let price = Price::from("100.00"); + + fn fill_limit_order_handler(order: LimitOrderType) { + FILLED_LIMITS.lock().unwrap().push(order); + } + + let mut matching_core = OrderMatchingCore::new( + instrument_id, + Price::from("0.01"), + None, + None, + Some(fill_limit_order_handler), + ); + + matching_core.bid = Some(Price::from("100.00")); + matching_core.ask = Some(Price::from("100.00")); + + let order = TestOrderStubs::limit_order( + instrument_id, + order_side, + price, + Quantity::from("100.00"), + None, + None, + ); + + matching_core.match_limit_order(&LimitOrderType::Limit(order.clone())); + + let filled_limits = FILLED_LIMITS.lock().unwrap(); + assert_eq!(filled_limits.len(), 1); + assert_eq!(filled_limits[0], LimitOrderType::Limit(order)); + } } From 8fd67977d8c2a5b8ffc2ad319a39a8282bc61a60 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 17:43:15 +1100 Subject: [PATCH 11/71] Add OrderMatchingEngine scaffolding --- nautilus_core/Cargo.lock | 1 + nautilus_core/backtest/Cargo.toml | 18 ++++- nautilus_core/backtest/src/lib.rs | 1 + nautilus_core/backtest/src/matching_engine.rs | 66 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 nautilus_core/backtest/src/matching_engine.rs diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 95c5fdf28184..0dcdd2b6d00f 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2469,6 +2469,7 @@ dependencies = [ "cbindgen", "nautilus-common", "nautilus-core", + "nautilus-execution", "nautilus-model", "pyo3", "rstest", diff --git a/nautilus_core/backtest/Cargo.toml b/nautilus_core/backtest/Cargo.toml index 9091b2caa12b..6e18d3664dbc 100644 --- a/nautilus_core/backtest/Cargo.toml +++ b/nautilus_core/backtest/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["rlib", "staticlib"] [dependencies] nautilus-common = { path = "../common" } nautilus-core = { path = "../core" } +nautilus-execution = { path = "../execution" } nautilus-model = { path = "../model" } pyo3 = { workspace = true, optional = true } ustr = { workspace = true } @@ -29,8 +30,21 @@ extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", "nautilus-core/extension-module", + "nautilus-execution/extension-module", "nautilus-model/extension-module", ] -ffi = ["cbindgen", "nautilus-core/ffi", "nautilus-common/ffi"] -python = ["pyo3", "nautilus-core/python", "nautilus-common/python"] +ffi = [ + "cbindgen", + "nautilus-core/ffi", + "nautilus-common/ffi", + "nautilus-execution/ffi", + "nautilus-model/ffi", +] +python = [ + "pyo3", + "nautilus-core/python", + "nautilus-common/python", + "nautilus-execution/python", + "nautilus-model/python", +] default = ["ffi", "python"] diff --git a/nautilus_core/backtest/src/lib.rs b/nautilus_core/backtest/src/lib.rs index 83e930fd7e61..186dfbc301a7 100644 --- a/nautilus_core/backtest/src/lib.rs +++ b/nautilus_core/backtest/src/lib.rs @@ -14,3 +14,4 @@ // ------------------------------------------------------------------------------------------------- pub mod engine; +pub mod matching_engine; diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs new file mode 100644 index 000000000000..abdfc3ab3871 --- /dev/null +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +#![allow(dead_code)] // Under development + +use std::collections::HashMap; + +use nautilus_common::msgbus::MessageBus; +use nautilus_core::time::AtomicTime; +use nautilus_execution::matching_core::OrderMatchingCore; +use nautilus_model::{ + data::bar::Bar, + enums::{AccountType, BookType, MarketStatus, OmsType}, + identifiers::{account_id::AccountId, trader_id::TraderId, venue::Venue}, + instruments::Instrument, + orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp}, + types::price::Price, +}; + +pub struct OrderMatchingEngineConfig { + pub bar_execution: bool, + pub reject_stop_orders: bool, + pub support_gtd_orders: bool, + pub support_contingent_orders: bool, + pub use_position_ids: bool, + pub use_random_ids: bool, + pub use_reduce_only: bool, +} + +pub struct OrderMatchingEngine { + pub venue: Venue, + pub instrument: Box, + pub raw_id: u64, + pub book_type: BookType, + pub oms_type: OmsType, + pub account_type: AccountType, + pub market_status: MarketStatus, + pub config: OrderMatchingEngineConfig, + // pub cache: Cache // TODO! + clock: &'static AtomicTime, + msgbus: &'static MessageBus, + book_mbo: Option, + book_mbp: Option, + account_ids: HashMap, + core: OrderMatchingCore, + target_bid: Option, + target_ask: Option, + target_last: Option, + last_bar_bid: Option, + last_bar_ask: Option, + position_count: usize, + order_count: usize, + execution_count: usize, +} From 4d8bebb8349f0725fea17eb003c7601e15cae8e4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 16 Mar 2024 17:48:57 +1100 Subject: [PATCH 12/71] Standardize cargo manifest formatting --- nautilus_core/adapters/Cargo.toml | 8 ++++++-- nautilus_core/execution/Cargo.toml | 8 ++++++-- nautilus_core/indicators/Cargo.toml | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 519cb242a0ee..6e3f1a343f3a 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -52,12 +52,16 @@ extension-module = [ "nautilus-model/extension-module", ] databento = ["dep:databento", "dbn", "python"] -ffi = ["nautilus-core/ffi", "nautilus-model/ffi", "nautilus-common/ffi"] +ffi = [ + "nautilus-common/ffi", + "nautilus-core/ffi", + "nautilus-model/ffi", +] python = [ "pyo3", "pyo3-asyncio", + "nautilus-common/python", "nautilus-core/python", "nautilus-model/python", - "nautilus-common/python", ] default = ["ffi", "python"] diff --git a/nautilus_core/execution/Cargo.toml b/nautilus_core/execution/Cargo.toml index 94333a9ac259..1c2df67aa6a9 100644 --- a/nautilus_core/execution/Cargo.toml +++ b/nautilus_core/execution/Cargo.toml @@ -39,12 +39,16 @@ extension-module = [ "nautilus-core/extension-module", "nautilus-model/extension-module", ] -ffi = ["nautilus-core/ffi", "nautilus-model/ffi", "nautilus-common/ffi"] +ffi = [ + "nautilus-common/ffi", + "nautilus-core/ffi", + "nautilus-model/ffi", +] python = [ "pyo3", "pyo3-asyncio", + "nautilus-common/python", "nautilus-core/python", "nautilus-model/python", - "nautilus-common/python", ] default = ["ffi", "python"] diff --git a/nautilus_core/indicators/Cargo.toml b/nautilus_core/indicators/Cargo.toml index 4c4bf01ef863..9fe555b848ea 100644 --- a/nautilus_core/indicators/Cargo.toml +++ b/nautilus_core/indicators/Cargo.toml @@ -26,5 +26,9 @@ extension-module = [ "nautilus-core/extension-module", "nautilus-model/extension-module", ] -python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] +python = [ + "pyo3", + "nautilus-core/python", + "nautilus-model/python", +] default = [] From 6593894dabe2d69bc007f117fa71c710c369654c Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 17 Mar 2024 00:06:00 +0100 Subject: [PATCH 13/71] Futures Contract pyo3 Rust Cython conversion (#1544) --- .../adapters/src/databento/decode.rs | 18 +++++-- .../model/src/instruments/futures_contract.rs | 15 +++++- nautilus_core/model/src/instruments/stubs.rs | 4 ++ .../python/instruments/futures_contract.rs | 47 ++++++++++++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 6 +++ .../model/instruments/crypto_perpetual.pyx | 1 - .../model/instruments/futures_contract.pyx | 6 ++- .../test_kit/rust/instruments_pyo3.py | 2 + .../instruments/test_futures_contract_pyo3.py | 27 +++++++++-- tests/unit_tests/model/test_instrument.py | 4 ++ 10 files changed, 115 insertions(+), 15 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index ce213526c9ea..42d10591dd75 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -235,7 +235,11 @@ pub fn decode_futures_contract_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 2, Quantity::new(1.0, 0)?, // TBD + Quantity::new(1.0, 0)?, // TBD + None, // TBD + None, // TBD None, // TBD None, // TBD None, // TBD @@ -784,12 +788,16 @@ pub fn decode_futures_contract( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 2, // TBD Quantity::new(1.0, 0)?, // TBD - None, // TBD - None, // TBD - None, // TBD - None, // TBD - msg.ts_recv, // More accurate and reliable timestamp + Quantity::new(1.0, 0)?, // TBD + None, + None, // TBD + None, // TBD + None, // TBD + None, // TBD + None, // TBD + msg.ts_recv, // More accurate and reliable timestamp ts_init, ) } diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 7003983f9398..61522b97664b 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -20,6 +20,7 @@ use std::{ use anyhow::Result; use nautilus_core::time::UnixNanos; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -49,8 +50,12 @@ pub struct FuturesContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -72,12 +77,16 @@ impl FuturesContract { currency: Currency, price_precision: u8, price_increment: Price, + size_increment: Quantity, + size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, ) -> Result { @@ -92,10 +101,14 @@ impl FuturesContract { currency, price_precision, price_increment, + size_precision, + size_increment, multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 584e3f83114a..7595e6abdf4e 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -312,12 +312,16 @@ pub fn futures_contract_es() -> FuturesContract { Currency::USD(), 2, Price::from("0.01"), + Quantity::from("0.00001"), + 5, Quantity::from(1), Quantity::from(1), None, None, None, None, + None, + None, 0, 0, ) diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs index 248237f2481c..e80a7d36e259 100644 --- a/nautilus_core/model/src/python/instruments/futures_contract.rs +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -47,10 +47,14 @@ impl FuturesContract { currency: Currency, price_precision: u8, price_increment: Price, + size_precision: u8, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -68,12 +72,16 @@ impl FuturesContract { currency, price_precision, price_increment, + size_increment, + size_precision, multiplier, lot_size, max_quantity, min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -195,6 +203,18 @@ impl FuturesContract { self.min_price } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -207,6 +227,24 @@ impl FuturesContract { self.ts_init } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[staticmethod] #[pyo3(name = "from_dict")] fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { @@ -226,8 +264,13 @@ impl FuturesContract { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index ee7a99997d1d..e050a78a9732 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1238,10 +1238,14 @@ class FuturesContract: currency: Currency, price_precision: int, price_increment: Price, + size_precision: int, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, ts_init: int, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, max_quantity: Quantity | None = None, min_quantity: Quantity | None = None, max_price: Price | None = None, @@ -1249,6 +1253,8 @@ class FuturesContract: exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None: ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> CryptoFuture: ... @property def id(self) -> InstrumentId: ... @property diff --git a/nautilus_trader/model/instruments/crypto_perpetual.pyx b/nautilus_trader/model/instruments/crypto_perpetual.pyx index 062e05c3e8c4..9a61964351cc 100644 --- a/nautilus_trader/model/instruments/crypto_perpetual.pyx +++ b/nautilus_trader/model/instruments/crypto_perpetual.pyx @@ -246,7 +246,6 @@ cdef class CryptoPerpetual(Instrument): "margin_maint": str(obj.margin_maint), "maker_fee": str(obj.maker_fee), "taker_fee": str(obj.taker_fee), - "ts_event": obj.ts_event, "ts_init": obj.ts_init, "info": obj.info, diff --git a/nautilus_trader/model/instruments/futures_contract.pyx b/nautilus_trader/model/instruments/futures_contract.pyx index e4a2cd063597..1ff36bc4d396 100644 --- a/nautilus_trader/model/instruments/futures_contract.pyx +++ b/nautilus_trader/model/instruments/futures_contract.pyx @@ -26,7 +26,6 @@ from nautilus_trader.core.rust.model cimport AssetClass from nautilus_trader.core.rust.model cimport InstrumentClass from nautilus_trader.model.functions cimport asset_class_from_str from nautilus_trader.model.functions cimport asset_class_to_str -from nautilus_trader.model.functions cimport instrument_class_from_str from nautilus_trader.model.functions cimport instrument_class_to_str from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Symbol @@ -223,6 +222,10 @@ cdef class FuturesContract(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": obj.underlying, "activation_ns": obj.activation_ns, @@ -249,6 +252,7 @@ cdef class FuturesContract(Instrument): underlying=pyo3_instrument.underlying, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 30156cdc4f15..1bda90122fc9 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -334,6 +334,8 @@ def futures_contract_es( currency=_USD, price_precision=2, price_increment=Price.from_str("0.01"), + size_precision=0, + size_increment=Quantity.from_str("1"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, diff --git a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py index 1dcebd0f9687..1184a5a6b397 100644 --- a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import FuturesContract -from nautilus_trader.model.instruments import FuturesContract as LegacyFuturesContract +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import FuturesContract from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _ES_FUTURE.to_dict() - assert FuturesContract.from_dict(result) == _ES_FUTURE + assert nautilus_pyo3.FuturesContract.from_dict(result) == _ES_FUTURE assert result == { "type": "FuturesContract", "id": "ESZ1.GLBX", @@ -45,12 +45,17 @@ def test_to_dict(): "currency": "USD", "price_precision": 2, "price_increment": "0.01", + "size_precision": 0, + "size_increment": "1", "multiplier": "1", "lot_size": "1", "max_price": None, "max_quantity": None, "min_price": None, - "min_quantity": None, + "min_quantity": "1", + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, "exchange": "XCME", @@ -58,6 +63,18 @@ def test_to_dict(): def test_legacy_futures_contract_from_pyo3(): - future = LegacyFuturesContract.from_pyo3(_ES_FUTURE) + future = FuturesContract.from_pyo3(_ES_FUTURE) assert future.id.value == "ESZ1.GLBX" + + +def test_pyo3_cython_conversion(): + futures_contract_pyo3 = TestInstrumentProviderPyo3.futures_contract_es() + futures_contract_pyo3_dict = futures_contract_pyo3.to_dict() + futures_contract_cython = FuturesContract.from_pyo3(futures_contract_pyo3) + futures_contract_cython_dict = FuturesContract.to_dict(futures_contract_cython) + futures_contract_pyo3_back = nautilus_pyo3.FuturesContract.from_dict( + futures_contract_cython_dict, + ) + assert futures_contract_pyo3 == futures_contract_pyo3_back + assert futures_contract_pyo3_dict == futures_contract_cython_dict diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index d53d804328aa..25c9b9d7838f 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -266,6 +266,10 @@ def test_future_instrument_to_dict(self): "currency": "USD", "activation_ns": 1622842200000000000, "expiration_ns": 1702650600000000000, + "max_price": None, + "max_quantity": None, + "min_price": None, + "min_quantity": "1", "lot_size": "1", "margin_init": "0", "margin_maint": "0", From 8e1be531c6c1c952b7cceb9a5158290b0d9eeb24 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 17 Mar 2024 00:07:12 +0100 Subject: [PATCH 14/71] Futures Spread pyo3 Rust Cython conversion (#1545) --- .../adapters/src/databento/decode.rs | 8 ++++ .../model/src/instruments/futures_spread.rs | 15 +++++- nautilus_core/model/src/instruments/stubs.rs | 4 ++ .../src/python/instruments/futures_spread.rs | 47 ++++++++++++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 6 +++ .../model/instruments/futures_spread.pyx | 5 ++ .../test_kit/rust/instruments_pyo3.py | 2 + .../instruments/test_futures_spread_pyo3.py | 25 ++++++++-- 8 files changed, 104 insertions(+), 8 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index 42d10591dd75..f6d3a8ea05a0 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -274,7 +274,11 @@ pub fn decode_futures_spread_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, Quantity::new(1.0, 0)?, // TBD + Quantity::new(1.0, 0)?, // TBD + None, // TBD + None, // TBD None, // TBD None, // TBD None, // TBD @@ -827,7 +831,11 @@ pub fn decode_futures_spread( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, Quantity::new(1.0, 0)?, // TBD + Quantity::new(1.0, 0)?, // TBD + None, // TBD + None, // TBD None, // TBD None, // TBD None, // TBD diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index a8ba3fcb2390..195b239c4548 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -20,6 +20,7 @@ use std::{ use anyhow::Result; use nautilus_core::time::UnixNanos; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -50,8 +51,12 @@ pub struct FuturesSpread { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -74,12 +79,16 @@ impl FuturesSpread { currency: Currency, price_precision: u8, price_increment: Price, + size_increment: Quantity, + size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, ) -> Result { @@ -95,10 +104,14 @@ impl FuturesSpread { currency, price_precision, price_increment, + size_precision, + size_increment, multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 7595e6abdf4e..77e4b7bbc857 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -348,12 +348,16 @@ pub fn futures_spread_es() -> FuturesSpread { Currency::USD(), 2, Price::from("0.01"), + Quantity::from("0.01"), + 2, Quantity::from(1), Quantity::from(1), None, None, None, None, + None, + None, 0, 0, ) diff --git a/nautilus_core/model/src/python/instruments/futures_spread.rs b/nautilus_core/model/src/python/instruments/futures_spread.rs index 770f1111ff8e..755fc5153a88 100644 --- a/nautilus_core/model/src/python/instruments/futures_spread.rs +++ b/nautilus_core/model/src/python/instruments/futures_spread.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -48,10 +48,14 @@ impl FuturesSpread { currency: Currency, price_precision: u8, price_increment: Price, + size_precision: u8, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -70,12 +74,16 @@ impl FuturesSpread { currency, price_precision, price_increment, + size_increment, + size_precision, multiplier, lot_size, max_quantity, min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -167,6 +175,18 @@ impl FuturesSpread { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -203,6 +223,24 @@ impl FuturesSpread { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -235,8 +273,13 @@ impl FuturesSpread { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index e050a78a9732..6f55162de5e6 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1286,6 +1286,8 @@ class FuturesSpread: currency: Currency, price_precision: int, price_increment: Price, + size_precision: int, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1294,9 +1296,13 @@ class FuturesSpread: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None: ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> FuturesSpread: ... @property def id(self) -> InstrumentId: ... @property diff --git a/nautilus_trader/model/instruments/futures_spread.pyx b/nautilus_trader/model/instruments/futures_spread.pyx index e609324781c0..02cc0888921a 100644 --- a/nautilus_trader/model/instruments/futures_spread.pyx +++ b/nautilus_trader/model/instruments/futures_spread.pyx @@ -234,6 +234,10 @@ cdef class FuturesSpread(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": obj.underlying, "strategy_type": obj.strategy_type, @@ -262,6 +266,7 @@ cdef class FuturesSpread(Instrument): strategy_type=pyo3_instrument.strategy_type, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 1bda90122fc9..707301d858e4 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -367,6 +367,8 @@ def futures_spread_es( currency=_USD, price_precision=2, price_increment=Price.from_str("0.01"), + size_precision=0, + size_increment=Quantity.from_str("1"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, diff --git a/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py b/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py index 203537876dbf..3af59ecb5cce 100644 --- a/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py +++ b/tests/unit_tests/model/instruments/test_futures_spread_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import FuturesSpread -from nautilus_trader.model.instruments import FuturesSpread as LegacyFuturesSpread +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import FuturesSpread from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _ES_FUTURES_SPREAD.to_dict() - assert FuturesSpread.from_dict(result) == _ES_FUTURES_SPREAD + assert nautilus_pyo3.FuturesSpread.from_dict(result) == _ES_FUTURES_SPREAD assert result == { "type": "FuturesSpread", "id": "ESM4-ESU4.GLBX", @@ -46,12 +46,17 @@ def test_to_dict(): "currency": "USD", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_price": None, "max_quantity": None, "min_price": None, - "min_quantity": None, + "min_quantity": "1", + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, "exchange": "XCME", @@ -59,6 +64,16 @@ def test_to_dict(): def test_legacy_futures_contract_from_pyo3(): - future = LegacyFuturesSpread.from_pyo3(_ES_FUTURES_SPREAD) + future = FuturesSpread.from_pyo3(_ES_FUTURES_SPREAD) assert future.id.value == "ESM4-ESU4.GLBX" + + +def test_pyo3_cython_conversion(): + futures_spread_pyo3 = TestInstrumentProviderPyo3.futures_spread_es() + futures_spread_pyo3_dict = futures_spread_pyo3.to_dict() + futures_spread_cython = FuturesSpread.from_pyo3(futures_spread_pyo3) + futures_spread_cython_dict = FuturesSpread.to_dict(futures_spread_cython) + futures_spread_pyo3_back = nautilus_pyo3.FuturesSpread.from_dict(futures_spread_cython_dict) + assert futures_spread_pyo3_dict == futures_spread_cython_dict + assert futures_spread_pyo3 == futures_spread_pyo3_back From 1c5438f42dd033cf0f351c74ee88cdbc1c910ed8 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 17 Mar 2024 00:09:21 +0100 Subject: [PATCH 15/71] Options Contract pyo3 Rust Cython conversion (#1546) --- .../adapters/src/databento/decode.rs | 10 +++- .../model/src/instruments/options_contract.rs | 15 +++++- nautilus_core/model/src/instruments/stubs.rs | 4 ++ .../python/instruments/options_contract.rs | 47 ++++++++++++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 6 +++ .../model/instruments/options_contract.pyx | 5 ++ .../test_kit/rust/instruments_pyo3.py | 2 + .../instruments/test_options_contract_pyo3.py | 27 +++++++++-- tests/unit_tests/model/test_instrument.py | 4 ++ 9 files changed, 111 insertions(+), 9 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index f6d3a8ea05a0..6a3c2c3d2f89 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -320,12 +320,16 @@ pub fn decode_options_contract_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, + Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD None, // TBD None, // TBD - msg.ts_recv, // More accurate and reliable timestamp + None, + None, + msg.ts_recv, // More accurate and reliable timestamp ts_init, ) } @@ -877,11 +881,15 @@ pub fn decode_options_contract( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, + Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD None, // TBD None, // TBD + None, // TBD + None, // TBD msg.ts_recv, // More accurate and reliable timestamp ts_init, ) diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index d3307d6de831..6724d53657a0 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -20,6 +20,7 @@ use std::{ use anyhow::Result; use nautilus_core::time::UnixNanos; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -51,8 +52,12 @@ pub struct OptionsContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -76,12 +81,16 @@ impl OptionsContract { currency: Currency, price_precision: u8, price_increment: Price, + size_increment: Quantity, + size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, + margin_init: Option, + margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, ) -> Result { @@ -98,12 +107,16 @@ impl OptionsContract { currency, price_precision, price_increment, + size_precision, + size_increment, multiplier, lot_size, max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), ts_event, ts_init, }) diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 77e4b7bbc857..a4099c42ea5d 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -386,11 +386,15 @@ pub fn options_contract_appl() -> OptionsContract { 2, Price::from("0.01"), Quantity::from(1), + 0, + Quantity::from(1), Quantity::from(1), None, None, None, None, + None, + None, 0, 0, ) diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs index 9fca9890e8b2..c41e156487de 100644 --- a/nautilus_core/model/src/python/instruments/options_contract.rs +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -49,10 +49,14 @@ impl OptionsContract { currency: Currency, price_precision: u8, price_increment: Price, + size_precision: u8, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -72,12 +76,16 @@ impl OptionsContract { currency, price_precision, price_increment, + size_increment, + size_precision, multiplier, lot_size, max_quantity, min_quantity, max_price, min_price, + margin_init, + margin_maint, ts_event, ts_init, ) @@ -175,6 +183,18 @@ impl OptionsContract { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -211,6 +231,24 @@ impl OptionsContract { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -244,8 +282,13 @@ impl OptionsContract { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 6f55162de5e6..2eb7413629a9 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1335,6 +1335,8 @@ class OptionsContract: currency: Currency, price_precision: int, price_increment: Price, + size_precision: int, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1343,9 +1345,13 @@ class OptionsContract: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None : ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> OptionsContract: ... @property def id(self) -> InstrumentId: ... @property diff --git a/nautilus_trader/model/instruments/options_contract.pyx b/nautilus_trader/model/instruments/options_contract.pyx index 8e3b255480c0..78de9096813a 100644 --- a/nautilus_trader/model/instruments/options_contract.pyx +++ b/nautilus_trader/model/instruments/options_contract.pyx @@ -237,6 +237,10 @@ cdef class OptionsContract(Instrument): "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": str(obj.underlying), "option_kind": option_kind_to_str(obj.option_kind), @@ -268,6 +272,7 @@ cdef class OptionsContract(Instrument): activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, strike_price=Price.from_raw_c(pyo3_instrument.strike_price.raw, pyo3_instrument.strike_price.precision), + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 707301d858e4..390d626a4025 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -286,6 +286,8 @@ def aapl_option( currency=_USDT, price_precision=2, price_increment=Price.from_str("0.01"), + size_precision=0, + size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, diff --git a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py index c42b665e2f26..9aa2b4c9e775 100644 --- a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import OptionsContract -from nautilus_trader.model.instruments import OptionsContract as LegacyOptionsContract +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import OptionsContract from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _AAPL_OPTION.to_dict() - assert OptionsContract.from_dict(result) == _AAPL_OPTION + assert nautilus_pyo3.OptionsContract.from_dict(result) == _AAPL_OPTION assert result == { "type": "OptionsContract", "id": "AAPL211217C00150000.OPRA", @@ -48,18 +48,35 @@ def test_to_dict(): "currency": "USDT", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_quantity": None, - "min_quantity": None, + "min_quantity": "1", "max_price": None, "min_price": None, + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, } def test_legacy_options_contract_from_pyo3(): - option = LegacyOptionsContract.from_pyo3(_AAPL_OPTION) + option = OptionsContract.from_pyo3(_AAPL_OPTION) assert option.id.value == "AAPL211217C00150000.OPRA" + + +def test_pyo3_cython_conversion(): + options_contract_pyo3 = TestInstrumentProviderPyo3.aapl_option() + options_contract_pyo3_dict = options_contract_pyo3.to_dict() + options_contract_cython = OptionsContract.from_pyo3(options_contract_pyo3) + options_contract_cython_dict = OptionsContract.to_dict(options_contract_cython) + options_contract_pyo3_back = nautilus_pyo3.OptionsContract.from_dict( + options_contract_cython_dict, + ) + assert options_contract_cython_dict == options_contract_pyo3_dict + assert options_contract_pyo3 == options_contract_pyo3_back diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index 25c9b9d7838f..cf053181387e 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -301,6 +301,10 @@ def test_option_instrument_to_dict(self): "expiration_ns": 1639699200000000000, "option_kind": "CALL", "lot_size": "1", + "max_price": None, + "max_quantity": None, + "min_price": None, + "min_quantity": "1", "margin_init": "0", "margin_maint": "0", "multiplier": "100", From 92d5ab318cac3169e59e80c2f5f9af9209b0bfba Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 17 Mar 2024 00:10:27 +0100 Subject: [PATCH 16/71] Options Spread pyo3 Rust Cython conversion (#1547) --- .../adapters/src/databento/decode.rs | 8 ++++ .../model/src/instruments/options_spread.rs | 15 +++++- nautilus_core/model/src/instruments/stubs.rs | 4 ++ .../src/python/instruments/options_spread.rs | 47 ++++++++++++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 6 +++ .../model/instruments/options_spread.pyx | 6 +++ .../test_kit/rust/instruments_pyo3.py | 2 + .../instruments/test_options_spread_pyo3.py | 25 ++++++++-- 8 files changed, 105 insertions(+), 8 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index 6a3c2c3d2f89..a7f946feabd1 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -366,7 +366,11 @@ pub fn decode_options_spread_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, // TBD Quantity::new(1.0, 0)?, // TBD + Quantity::new(1.0, 0)?, // TBD + None, // TBD + None, // TBD None, // TBD None, // TBD None, // TBD @@ -927,7 +931,11 @@ pub fn decode_options_spread( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD + 0, Quantity::new(1.0, 0)?, // TBD + Quantity::new(1.0, 0)?, // TBD + None, // TBD + None, // TBD None, // TBD None, // TBD None, // TBD diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 2a4522644d78..49f06b9be335 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -20,6 +20,7 @@ use std::{ use anyhow::Result; use nautilus_core::time::UnixNanos; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -50,8 +51,12 @@ pub struct OptionsSpread { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub size_increment: Quantity, + pub size_precision: u8, pub multiplier: Quantity, pub lot_size: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, @@ -74,8 +79,12 @@ impl OptionsSpread { currency: Currency, price_precision: u8, price_increment: Price, + size_increment: Quantity, + size_precision: u8, multiplier: Quantity, lot_size: Quantity, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -95,10 +104,14 @@ impl OptionsSpread { currency, price_precision, price_increment, + size_precision, + size_increment, multiplier, lot_size, + margin_init: margin_init.unwrap_or(0.into()), + margin_maint: margin_maint.unwrap_or(0.into()), max_quantity, - min_quantity, + min_quantity: Some(min_quantity.unwrap_or(1.into())), max_price, min_price, ts_event, diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index a4099c42ea5d..236ecebb0f27 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -422,11 +422,15 @@ pub fn options_spread() -> OptionsSpread { 2, Price::from("0.01"), Quantity::from(1), + 0, + Quantity::from(1), Quantity::from(1), None, None, None, None, + None, + None, 0, 0, ) diff --git a/nautilus_core/model/src/python/instruments/options_spread.rs b/nautilus_core/model/src/python/instruments/options_spread.rs index 0b53fa5c79bd..3839377aebef 100644 --- a/nautilus_core/model/src/python/instruments/options_spread.rs +++ b/nautilus_core/model/src/python/instruments/options_spread.rs @@ -23,7 +23,7 @@ use nautilus_core::{ time::UnixNanos, }; use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use ustr::Ustr; use crate::{ @@ -48,10 +48,14 @@ impl OptionsSpread { currency: Currency, price_precision: u8, price_increment: Price, + size_precision: u8, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, + margin_init: Option, + margin_maint: Option, max_quantity: Option, min_quantity: Option, max_price: Option, @@ -70,8 +74,12 @@ impl OptionsSpread { currency, price_precision, price_increment, + size_increment, + size_precision, multiplier, lot_size, + margin_init, + margin_maint, max_quantity, min_quantity, max_price, @@ -167,6 +175,18 @@ impl OptionsSpread { self.price_increment } + #[getter] + #[pyo3(name = "size_increment")] + fn py_size_increment(&self) -> Quantity { + self.size_increment + } + + #[getter] + #[pyo3(name = "size_precision")] + fn py_size_precision(&self) -> u8 { + self.size_precision + } + #[getter] #[pyo3(name = "multiplier")] fn py_multiplier(&self) -> Quantity { @@ -203,6 +223,24 @@ impl OptionsSpread { self.min_price } + #[getter] + #[pyo3(name = "margin_init")] + fn py_margin_init(&self) -> Decimal { + self.margin_init + } + + #[getter] + #[pyo3(name = "margin_maint")] + fn py_margin_maint(&self) -> Decimal { + self.margin_maint + } + + #[getter] + #[pyo3(name = "info")] + fn py_info(&self, py: Python<'_>) -> PyResult { + Ok(PyDict::new(py).into()) + } + #[getter] #[pyo3(name = "ts_event")] fn py_ts_event(&self) -> UnixNanos { @@ -235,8 +273,13 @@ impl OptionsSpread { dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("size_precision", self.size_precision)?; dict.set_item("multiplier", self.multiplier.to_string())?; - dict.set_item("lot_size", self.multiplier.to_string())?; + dict.set_item("lot_size", self.lot_size.to_string())?; + dict.set_item("margin_init", self.margin_init.to_string())?; + dict.set_item("margin_maint", self.margin_maint.to_string())?; + dict.set_item("info", PyDict::new(py))?; dict.set_item("ts_event", self.ts_event)?; dict.set_item("ts_init", self.ts_init)?; match self.max_quantity { diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 2eb7413629a9..f0d59ef87448 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1383,6 +1383,8 @@ class OptionsSpread: currency: Currency, price_precision: int, price_increment: Price, + size_precision: int, + size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1391,9 +1393,13 @@ class OptionsSpread: min_quantity: Quantity | None = None, max_price: Price | None = None, min_price: Price | None = None, + margin_init: Decimal | None = None, + margin_maint: Decimal | None = None, exchange: str | None = None, info: dict[str, Any] | None = None, ) -> None : ... + @classmethod + def from_dict(cls, values: dict[str, str]) -> OptionsContract: ... @property def id(self) -> InstrumentId: ... @property diff --git a/nautilus_trader/model/instruments/options_spread.pyx b/nautilus_trader/model/instruments/options_spread.pyx index 6b917992bb27..0f5514d3dca1 100644 --- a/nautilus_trader/model/instruments/options_spread.pyx +++ b/nautilus_trader/model/instruments/options_spread.pyx @@ -231,12 +231,17 @@ cdef class OptionsSpread(Instrument): "id": obj.id.to_str(), "raw_symbol": obj.raw_symbol.to_str(), "asset_class": asset_class_to_str(obj.asset_class), + "strategy_type": obj.strategy_type, "currency": obj.quote_currency.code, "price_precision": obj.price_precision, "price_increment": str(obj.price_increment), "size_precision": obj.size_precision, "size_increment": str(obj.size_increment), "multiplier": str(obj.multiplier), + "max_quantity": str(obj.max_quantity) if obj.max_quantity is not None else None, + "min_quantity": str(obj.min_quantity) if obj.min_quantity is not None else None, + "max_price": str(obj.max_price) if obj.max_price is not None else None, + "min_price": str(obj.min_price) if obj.min_price is not None else None, "lot_size": str(obj.lot_size), "underlying": str(obj.underlying), "activation_ns": obj.activation_ns, @@ -265,6 +270,7 @@ cdef class OptionsSpread(Instrument): strategy_type=pyo3_instrument.strategy_type, activation_ns=pyo3_instrument.activation_ns, expiration_ns=pyo3_instrument.expiration_ns, + info=pyo3_instrument.info, ts_event=pyo3_instrument.ts_event, ts_init=pyo3_instrument.ts_init, exchange=pyo3_instrument.exchange, diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index 390d626a4025..df27e2d55282 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -402,6 +402,8 @@ def options_spread( currency=_USDT, price_precision=2, price_increment=Price.from_str("0.01"), + size_precision=0, + size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, diff --git a/tests/unit_tests/model/instruments/test_options_spread_pyo3.py b/tests/unit_tests/model/instruments/test_options_spread_pyo3.py index fa4742a768ba..8cb2ceb5afab 100644 --- a/tests/unit_tests/model/instruments/test_options_spread_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_spread_pyo3.py @@ -13,8 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.core.nautilus_pyo3 import OptionsSpread -from nautilus_trader.model.instruments import OptionsSpread as LegacyOptionsSpread +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.model.instruments import OptionsSpread from nautilus_trader.test_kit.rust.instruments_pyo3 import TestInstrumentProviderPyo3 @@ -33,7 +33,7 @@ def test_hash(): def test_to_dict(): result = _OPTIONS_SPREAD.to_dict() - assert OptionsSpread.from_dict(result) == _OPTIONS_SPREAD + assert nautilus_pyo3.OptionsSpread.from_dict(result) == _OPTIONS_SPREAD assert result == { "type": "OptionsSpread", "id": "UD:U$: GN 2534559.GLBX", @@ -47,18 +47,33 @@ def test_to_dict(): "currency": "USDT", "price_precision": 2, "price_increment": "0.01", + "size_increment": "1", + "size_precision": 0, "multiplier": "1", "lot_size": "1", "max_quantity": None, - "min_quantity": None, + "min_quantity": "1", "max_price": None, "min_price": None, + "margin_init": "0", + "margin_maint": "0", + "info": {}, "ts_event": 0, "ts_init": 0, } def test_legacy_options_contract_from_pyo3(): - option = LegacyOptionsSpread.from_pyo3(_OPTIONS_SPREAD) + option = OptionsSpread.from_pyo3(_OPTIONS_SPREAD) assert option.id.value == "UD:U$: GN 2534559.GLBX" + + +def test_pyo3_cython_conversion(): + options_spread_pyo3 = TestInstrumentProviderPyo3.options_spread() + options_spread_pyo3_dict = options_spread_pyo3.to_dict() + options_spread_cython = OptionsSpread.from_pyo3(options_spread_pyo3) + options_spread_cython_dict = OptionsSpread.to_dict(options_spread_cython) + options_spread_pyo3_back = nautilus_pyo3.OptionsSpread.from_dict(options_spread_cython_dict) + assert options_spread_cython_dict == options_spread_pyo3_dict + assert options_spread_pyo3 == options_spread_pyo3_back From 9639941b38a61b130f84fd9f5d235c840f57e1c0 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 10:13:17 +1100 Subject: [PATCH 17/71] Update core dependencies --- nautilus_core/Cargo.lock | 60 ++++++++++++++++++++-------------------- nautilus_core/Cargo.toml | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 0dcdd2b6d00f..c7c8a469f8ef 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -368,7 +368,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -566,7 +566,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "syn_derive", ] @@ -1057,7 +1057,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1068,7 +1068,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1363,7 +1363,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1405,7 +1405,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1415,7 +1415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -1676,7 +1676,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -2829,7 +2829,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -2897,7 +2897,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3113,7 +3113,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3363,7 +3363,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3376,7 +3376,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -3468,9 +3468,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c442de91f2a085154b1e1b374d5d5edf5bc49d2ebbfdf47e67edd6c2df568d" +checksum = "71d64e978fd98a0e6b105d066ba4889a7301fca65aeac850a877d8797343feeb" dependencies = [ "arc-swap", "async-trait", @@ -3716,7 +3716,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.52", + "syn 2.0.53", "unicode-ident", ] @@ -3931,7 +3931,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4142,7 +4142,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4393,7 +4393,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4406,7 +4406,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4428,9 +4428,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -4446,7 +4446,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4571,7 +4571,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4693,7 +4693,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4844,7 +4844,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -4964,7 +4964,7 @@ checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] @@ -5145,7 +5145,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-shared", ] @@ -5179,7 +5179,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5468,7 +5468,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.53", ] [[package]] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 203a1e9b3a7f..cd53abd0bb4e 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -35,7 +35,7 @@ log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_ pyo3 = { version = "0.20.3", features = ["anyhow", "rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -redis = { version = "0.25.1", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } +redis = { version = "0.25.2", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } rmp-serde = "1.1.2" rust_decimal = "1.34.3" rust_decimal_macros = "1.34.2" From 8667ce0c8703e9dfdeb7f80ed6e51b3dd3ece7a7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 10:40:51 +1100 Subject: [PATCH 18/71] Align Instrument constructors and validations --- .../adapters/src/databento/decode.rs | 16 ------- .../model/src/instruments/crypto_future.rs | 18 ++++++-- .../model/src/instruments/crypto_perpetual.rs | 18 ++++++-- .../model/src/instruments/currency_pair.rs | 18 ++++++-- nautilus_core/model/src/instruments/equity.rs | 12 +++-- .../model/src/instruments/futures_contract.rs | 18 +++++--- .../model/src/instruments/futures_spread.rs | 18 +++++--- .../model/src/instruments/options_contract.rs | 18 +++++--- .../model/src/instruments/options_spread.rs | 18 +++++--- nautilus_core/model/src/instruments/stubs.rs | 10 +---- .../model/src/instruments/synthetic.rs | 8 ++-- .../python/instruments/futures_contract.rs | 4 -- .../src/python/instruments/futures_spread.rs | 4 -- .../python/instruments/options_contract.rs | 4 -- .../src/python/instruments/options_spread.rs | 4 -- nautilus_trader/core/nautilus_pyo3.pyi | 8 ---- .../test_kit/rust/instruments_pyo3.py | 44 ++++++++----------- 17 files changed, 121 insertions(+), 119 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index a7f946feabd1..e3e813c23a3f 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -235,8 +235,6 @@ pub fn decode_futures_contract_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 2, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -274,8 +272,6 @@ pub fn decode_futures_spread_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -320,8 +316,6 @@ pub fn decode_options_contract_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -366,8 +360,6 @@ pub fn decode_options_spread_v1( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, // TBD - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -800,8 +792,6 @@ pub fn decode_futures_contract( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 2, // TBD - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, None, // TBD @@ -839,8 +829,6 @@ pub fn decode_futures_spread( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -885,8 +873,6 @@ pub fn decode_options_contract( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD @@ -931,8 +917,6 @@ pub fn decode_options_spread( currency.precision, decode_price(msg.min_price_increment, currency.precision)?, Quantity::new(1.0, 0)?, // TBD - 0, - Quantity::new(1.0, 0)?, // TBD Quantity::new(1.0, 0)?, // TBD None, // TBD None, // TBD diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 227038ade7de..7eda889c5114 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -91,7 +90,20 @@ impl CryptoFuture { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + check_u8_equal( + size_precision, + size_increment.precision, + "size_precision", + "size_increment.precision", + )?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index bef84460858d..bb655fb0a7d3 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -89,7 +88,20 @@ impl CryptoPerpetual { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + check_u8_equal( + size_precision, + size_increment.precision, + "size_precision", + "size_increment.precision", + )?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 188982667bbb..4c567b80b5e6 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -85,7 +84,20 @@ impl CurrencyPair { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + check_u8_equal( + size_precision, + size_increment.precision, + "size_precision", + "size_increment.precision", + )?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 5010444c4ad1..9e8011255efa 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -79,7 +78,14 @@ impl Equity { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + Ok(Self { id, raw_symbol, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 61522b97664b..8e1442ae8b06 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -77,8 +76,6 @@ impl FuturesContract { currency: Currency, price_precision: u8, price_increment: Price, - size_increment: Quantity, - size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, @@ -89,7 +86,14 @@ impl FuturesContract { margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + Ok(Self { id, raw_symbol, @@ -101,8 +105,8 @@ impl FuturesContract { currency, price_precision, price_increment, - size_precision, - size_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, margin_init: margin_init.unwrap_or(0.into()), diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index 195b239c4548..f1e525b93657 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -79,8 +78,6 @@ impl FuturesSpread { currency: Currency, price_precision: u8, price_increment: Price, - size_increment: Quantity, - size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, @@ -91,7 +88,14 @@ impl FuturesSpread { margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + Ok(Self { id, raw_symbol, @@ -104,8 +108,8 @@ impl FuturesSpread { currency, price_precision, price_increment, - size_precision, - size_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, margin_init: margin_init.unwrap_or(0.into()), diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 6724d53657a0..cf09a8d5611b 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -81,8 +80,6 @@ impl OptionsContract { currency: Currency, price_precision: u8, price_increment: Price, - size_increment: Quantity, - size_precision: u8, multiplier: Quantity, lot_size: Quantity, max_quantity: Option, @@ -93,7 +90,14 @@ impl OptionsContract { margin_maint: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + Ok(Self { id, raw_symbol, @@ -107,8 +111,8 @@ impl OptionsContract { currency, price_precision, price_increment, - size_precision, - size_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, max_quantity, diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 49f06b9be335..cc4ee7035773 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -18,8 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; -use nautilus_core::time::UnixNanos; +use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -79,8 +78,6 @@ impl OptionsSpread { currency: Currency, price_precision: u8, price_increment: Price, - size_increment: Quantity, - size_precision: u8, multiplier: Quantity, lot_size: Quantity, margin_init: Option, @@ -91,7 +88,14 @@ impl OptionsSpread { min_price: Option, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { + check_u8_equal( + price_precision, + price_increment.precision, + "price_precision", + "price_increment.precision", + )?; + Ok(Self { id, raw_symbol, @@ -104,8 +108,8 @@ impl OptionsSpread { currency, price_precision, price_increment, - size_precision, - size_increment, + size_precision: 0, + size_increment: Quantity::from("1"), multiplier, lot_size, margin_init: margin_init.unwrap_or(0.into()), diff --git a/nautilus_core/model/src/instruments/stubs.rs b/nautilus_core/model/src/instruments/stubs.rs index 236ecebb0f27..12a41667de31 100644 --- a/nautilus_core/model/src/instruments/stubs.rs +++ b/nautilus_core/model/src/instruments/stubs.rs @@ -82,7 +82,7 @@ pub fn crypto_perpetual_ethusdt() -> CryptoPerpetual { Currency::from("USDT"), false, 2, - 0, + 3, Price::from("0.01"), Quantity::from("0.001"), dec!(0.0002), @@ -312,8 +312,6 @@ pub fn futures_contract_es() -> FuturesContract { Currency::USD(), 2, Price::from("0.01"), - Quantity::from("0.00001"), - 5, Quantity::from(1), Quantity::from(1), None, @@ -348,8 +346,6 @@ pub fn futures_spread_es() -> FuturesSpread { Currency::USD(), 2, Price::from("0.01"), - Quantity::from("0.01"), - 2, Quantity::from(1), Quantity::from(1), None, @@ -386,8 +382,6 @@ pub fn options_contract_appl() -> OptionsContract { 2, Price::from("0.01"), Quantity::from(1), - 0, - Quantity::from(1), Quantity::from(1), None, None, @@ -422,8 +416,6 @@ pub fn options_spread() -> OptionsSpread { 2, Price::from("0.01"), Quantity::from(1), - 0, - Quantity::from(1), Quantity::from(1), None, None, diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index 75fbd9c2e636..ec59275c0188 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::{anyhow, Result}; +use anyhow::anyhow; use evalexpr::{ContextWithMutableVariables, HashMapContext, Node, Value}; use nautilus_core::time::UnixNanos; @@ -55,7 +55,7 @@ impl SyntheticInstrument { formula: String, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { let price_increment = Price::new(10f64.powi(-i32::from(price_precision)), price_precision)?; // Extract variables from the component instruments @@ -95,7 +95,7 @@ impl SyntheticInstrument { /// Calculates the price of the synthetic instrument based on the given component input prices /// provided as a map. #[allow(dead_code)] - pub fn calculate_from_map(&mut self, inputs: &HashMap) -> Result { + pub fn calculate_from_map(&mut self, inputs: &HashMap) -> anyhow::Result { let mut input_values = Vec::new(); for variable in &self.variables { @@ -113,7 +113,7 @@ impl SyntheticInstrument { /// Calculates the price of the synthetic instrument based on the given component input prices /// provided as an array of `f64` values. - pub fn calculate(&mut self, inputs: &[f64]) -> Result { + pub fn calculate(&mut self, inputs: &[f64]) -> anyhow::Result { if inputs.len() != self.variables.len() { return Err(anyhow!("Invalid number of input values")); } diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs index e80a7d36e259..0a022067dad3 100644 --- a/nautilus_core/model/src/python/instruments/futures_contract.rs +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -47,8 +47,6 @@ impl FuturesContract { currency: Currency, price_precision: u8, price_increment: Price, - size_precision: u8, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, @@ -72,8 +70,6 @@ impl FuturesContract { currency, price_precision, price_increment, - size_increment, - size_precision, multiplier, lot_size, max_quantity, diff --git a/nautilus_core/model/src/python/instruments/futures_spread.rs b/nautilus_core/model/src/python/instruments/futures_spread.rs index 755fc5153a88..8ad1b04ba87f 100644 --- a/nautilus_core/model/src/python/instruments/futures_spread.rs +++ b/nautilus_core/model/src/python/instruments/futures_spread.rs @@ -48,8 +48,6 @@ impl FuturesSpread { currency: Currency, price_precision: u8, price_increment: Price, - size_precision: u8, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, @@ -74,8 +72,6 @@ impl FuturesSpread { currency, price_precision, price_increment, - size_increment, - size_precision, multiplier, lot_size, max_quantity, diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs index c41e156487de..dfb2ce90de95 100644 --- a/nautilus_core/model/src/python/instruments/options_contract.rs +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -49,8 +49,6 @@ impl OptionsContract { currency: Currency, price_precision: u8, price_increment: Price, - size_precision: u8, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, @@ -76,8 +74,6 @@ impl OptionsContract { currency, price_precision, price_increment, - size_increment, - size_precision, multiplier, lot_size, max_quantity, diff --git a/nautilus_core/model/src/python/instruments/options_spread.rs b/nautilus_core/model/src/python/instruments/options_spread.rs index 3839377aebef..cc8585fa2d63 100644 --- a/nautilus_core/model/src/python/instruments/options_spread.rs +++ b/nautilus_core/model/src/python/instruments/options_spread.rs @@ -48,8 +48,6 @@ impl OptionsSpread { currency: Currency, price_precision: u8, price_increment: Price, - size_precision: u8, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: UnixNanos, @@ -74,8 +72,6 @@ impl OptionsSpread { currency, price_precision, price_increment, - size_increment, - size_precision, multiplier, lot_size, margin_init, diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index f0d59ef87448..45b2fbd6e588 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1238,8 +1238,6 @@ class FuturesContract: currency: Currency, price_precision: int, price_increment: Price, - size_precision: int, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1286,8 +1284,6 @@ class FuturesSpread: currency: Currency, price_precision: int, price_increment: Price, - size_precision: int, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1335,8 +1331,6 @@ class OptionsContract: currency: Currency, price_precision: int, price_increment: Price, - size_precision: int, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, @@ -1383,8 +1377,6 @@ class OptionsSpread: currency: Currency, price_precision: int, price_increment: Price, - size_precision: int, - size_increment: Quantity, multiplier: Quantity, lot_size: Quantity, ts_event: int, diff --git a/nautilus_trader/test_kit/rust/instruments_pyo3.py b/nautilus_trader/test_kit/rust/instruments_pyo3.py index df27e2d55282..ab637cad8519 100644 --- a/nautilus_trader/test_kit/rust/instruments_pyo3.py +++ b/nautilus_trader/test_kit/rust/instruments_pyo3.py @@ -264,6 +264,24 @@ def btcusdt_binance() -> CurrencyPair: ts_init=0, ) + @staticmethod + def aapl_equity() -> Equity: + return Equity( + id=InstrumentId.from_str("AAPL.XNAS"), + raw_symbol=Symbol("AAPL"), + isin="US0378331005", + currency=_USD, + price_precision=2, + price_increment=Price.from_str("0.01"), + lot_size=Quantity.from_int(100), + max_quantity=None, + min_quantity=None, + max_price=None, + min_price=None, + ts_event=0, + ts_init=0, + ) + @staticmethod def aapl_option( activation: pd.Timestamp | None = None, @@ -286,8 +304,6 @@ def aapl_option( currency=_USDT, price_precision=2, price_increment=Price.from_str("0.01"), - size_precision=0, - size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, @@ -298,24 +314,6 @@ def aapl_option( ts_init=0, ) - @staticmethod - def aapl_equity() -> Equity: - return Equity( - id=InstrumentId.from_str("AAPL.XNAS"), - raw_symbol=Symbol("AAPL"), - isin="US0378331005", - currency=_USD, - price_precision=2, - price_increment=Price.from_str("0.01"), - lot_size=Quantity.from_int(100), - max_quantity=None, - min_quantity=None, - max_price=None, - min_price=None, - ts_event=0, - ts_init=0, - ) - @staticmethod def futures_contract_es( activation: pd.Timestamp | None = None, @@ -336,8 +334,6 @@ def futures_contract_es( currency=_USD, price_precision=2, price_increment=Price.from_str("0.01"), - size_precision=0, - size_increment=Quantity.from_str("1"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, @@ -369,8 +365,6 @@ def futures_spread_es( currency=_USD, price_precision=2, price_increment=Price.from_str("0.01"), - size_precision=0, - size_increment=Quantity.from_str("1"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, @@ -402,8 +396,6 @@ def options_spread( currency=_USDT, price_precision=2, price_increment=Price.from_str("0.01"), - size_precision=0, - size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), max_quantity=None, From f7783797a15c0f08a163a2bdd62c706c7a6232cf Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 11:48:07 +1100 Subject: [PATCH 19/71] Standardize Rust anyhow crate usage --- nautilus_core/accounting/src/account/base.rs | 9 +- nautilus_core/accounting/src/account/cash.rs | 9 +- .../accounting/src/account/margin.rs | 9 +- nautilus_core/accounting/src/account/mod.rs | 7 +- .../adapters/src/databento/common.rs | 3 +- .../adapters/src/databento/decode.rs | 49 +++++----- nautilus_core/adapters/src/databento/live.rs | 9 +- .../adapters/src/databento/loader.rs | 15 ++- .../adapters/src/databento/python/decode.rs | 3 +- .../adapters/src/databento/symbology.rs | 11 +-- nautilus_core/core/src/correctness.rs | 44 ++++----- nautilus_core/core/src/datetime.rs | 14 +-- nautilus_core/core/src/parsing.rs | 6 +- nautilus_core/indicators/src/average/ama.rs | 3 +- nautilus_core/indicators/src/average/dema.rs | 3 +- nautilus_core/indicators/src/average/ema.rs | 3 +- nautilus_core/indicators/src/average/hma.rs | 3 +- nautilus_core/indicators/src/average/rma.rs | 3 +- nautilus_core/indicators/src/average/sma.rs | 3 +- nautilus_core/indicators/src/average/vidya.rs | 3 +- nautilus_core/indicators/src/average/wma.rs | 7 +- .../indicators/src/book/imbalance.rs | 3 +- .../indicators/src/momentum/aroon.rs | 3 +- nautilus_core/indicators/src/momentum/cmo.rs | 3 +- nautilus_core/indicators/src/momentum/rsi.rs | 3 +- .../indicators/src/ratio/efficiency_ratio.rs | 3 +- .../indicators/src/volatility/atr.rs | 3 +- nautilus_core/infrastructure/src/cache.rs | 15 ++- nautilus_core/infrastructure/src/redis.rs | 91 +++++++++++-------- nautilus_core/model/src/data/quote.rs | 3 +- .../model/src/events/account/state.rs | 3 +- .../model/src/events/order/accepted.rs | 3 +- .../model/src/events/order/cancel_rejected.rs | 3 +- .../model/src/events/order/canceled.rs | 3 +- .../model/src/events/order/denied.rs | 3 +- .../model/src/events/order/emulated.rs | 3 +- .../model/src/events/order/expired.rs | 3 +- .../model/src/events/order/filled.rs | 3 +- .../model/src/events/order/initialized.rs | 3 +- .../model/src/events/order/modify_rejected.rs | 3 +- .../model/src/events/order/pending_cancel.rs | 3 +- .../model/src/events/order/pending_update.rs | 3 +- .../model/src/events/order/rejected.rs | 3 +- .../model/src/events/order/released.rs | 3 +- .../model/src/events/order/submitted.rs | 3 +- .../model/src/events/order/triggered.rs | 3 +- .../model/src/events/order/updated.rs | 3 +- .../model/src/identifiers/account_id.rs | 3 +- .../model/src/identifiers/client_id.rs | 3 +- .../model/src/identifiers/client_order_id.rs | 3 +- .../model/src/identifiers/component_id.rs | 3 +- .../src/identifiers/exec_algorithm_id.rs | 3 +- .../model/src/identifiers/instrument_id.rs | 9 +- .../model/src/identifiers/order_list_id.rs | 3 +- .../model/src/identifiers/position_id.rs | 3 +- .../model/src/identifiers/strategy_id.rs | 3 +- nautilus_core/model/src/identifiers/symbol.rs | 3 +- .../model/src/identifiers/trade_id.rs | 7 +- .../model/src/identifiers/trader_id.rs | 3 +- nautilus_core/model/src/identifiers/venue.rs | 9 +- .../model/src/identifiers/venue_order_id.rs | 3 +- nautilus_core/model/src/instruments/mod.rs | 5 +- .../model/src/instruments/synthetic.rs | 7 +- nautilus_core/model/src/orders/default.rs | 10 +- nautilus_core/model/src/orders/limit.rs | 8 +- .../model/src/orders/limit_if_touched.rs | 8 +- nautilus_core/model/src/orders/market.rs | 7 +- .../model/src/orders/market_if_touched.rs | 9 +- .../model/src/orders/market_to_limit.rs | 8 +- nautilus_core/model/src/orders/stop_limit.rs | 8 +- nautilus_core/model/src/orders/stop_market.rs | 8 +- nautilus_core/model/src/orders/stubs.rs | 2 + .../model/src/orders/trailing_stop_limit.rs | 9 +- .../model/src/orders/trailing_stop_market.rs | 9 +- nautilus_core/model/src/position.rs | 3 +- nautilus_core/model/src/stubs.rs | 3 +- nautilus_core/model/src/types/balance.rs | 9 +- nautilus_core/model/src/types/currency.rs | 21 +++-- nautilus_core/model/src/types/fixed.rs | 6 +- nautilus_core/model/src/types/money.rs | 3 +- nautilus_core/model/src/types/price.rs | 5 +- nautilus_core/model/src/types/quantity.rs | 9 +- nautilus_core/persistence/src/db/database.rs | 3 +- 83 files changed, 286 insertions(+), 326 deletions(-) diff --git a/nautilus_core/accounting/src/account/base.rs b/nautilus_core/accounting/src/account/base.rs index fcfaf6163d95..147ffa9ca4d3 100644 --- a/nautilus_core/accounting/src/account/base.rs +++ b/nautilus_core/accounting/src/account/base.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -45,7 +44,7 @@ pub struct BaseAccount { } impl BaseAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { let mut balances_starting: HashMap = HashMap::new(); let mut balances: HashMap = HashMap::new(); event.balances.iter().for_each(|balance| { @@ -145,7 +144,7 @@ impl BaseAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { let base_currency = instrument .base_currency() .unwrap_or(instrument.quote_currency()); @@ -178,7 +177,7 @@ impl BaseAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { let mut pnls: HashMap = HashMap::new(); let quote_currency = instrument.quote_currency(); let base_currency = instrument.base_currency(); @@ -222,7 +221,7 @@ impl BaseAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { assert!( liquidity_side != LiquiditySide::NoLiquiditySide, "Invalid liquidity side" diff --git a/nautilus_core/accounting/src/account/cash.rs b/nautilus_core/accounting/src/account/cash.rs index eca71a1d88a9..7397d91b999f 100644 --- a/nautilus_core/accounting/src/account/cash.rs +++ b/nautilus_core/accounting/src/account/cash.rs @@ -19,7 +19,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -42,7 +41,7 @@ pub struct CashAccount { } impl CashAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, }) @@ -113,7 +112,7 @@ impl Account for CashAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse) } fn calculate_pnls( @@ -121,7 +120,7 @@ impl Account for CashAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { self.base_calculate_pnls(instrument, fill, position) } fn calculate_commission( @@ -131,7 +130,7 @@ impl Account for CashAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_commission( instrument, last_qty, diff --git a/nautilus_core/accounting/src/account/margin.rs b/nautilus_core/accounting/src/account/margin.rs index 8f5b578f799f..0ee6da01c474 100644 --- a/nautilus_core/accounting/src/account/margin.rs +++ b/nautilus_core/accounting/src/account/margin.rs @@ -22,7 +22,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::Result; use nautilus_model::{ enums::{AccountType, LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -54,7 +53,7 @@ pub struct MarginAccount { } impl MarginAccount { - pub fn new(event: AccountState, calculate_account_state: bool) -> Result { + pub fn new(event: AccountState, calculate_account_state: bool) -> anyhow::Result { Ok(Self { base: BaseAccount::new(event, calculate_account_state)?, leverages: HashMap::new(), @@ -322,7 +321,7 @@ impl Account for MarginAccount { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_balance_locked(instrument, side, quantity, price, use_quote_for_inverse) } fn calculate_pnls( @@ -330,7 +329,7 @@ impl Account for MarginAccount { instrument: T, fill: OrderFilled, position: Option, - ) -> Result> { + ) -> anyhow::Result> { self.base_calculate_pnls(instrument, fill, position) } fn calculate_commission( @@ -340,7 +339,7 @@ impl Account for MarginAccount { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result { + ) -> anyhow::Result { self.base_calculate_commission( instrument, last_qty, diff --git a/nautilus_core/accounting/src/account/mod.rs b/nautilus_core/accounting/src/account/mod.rs index 122b24c02a9d..1825998bf831 100644 --- a/nautilus_core/accounting/src/account/mod.rs +++ b/nautilus_core/accounting/src/account/mod.rs @@ -15,7 +15,6 @@ use std::collections::HashMap; -use anyhow::Result; use nautilus_model::{ enums::{LiquiditySide, OrderSide}, events::{account::state::AccountState, order::filled::OrderFilled}, @@ -48,14 +47,14 @@ pub trait Account { quantity: Quantity, price: Price, use_quote_for_inverse: Option, - ) -> Result; + ) -> anyhow::Result; fn calculate_pnls( &self, instrument: T, fill: OrderFilled, position: Option, - ) -> Result>; + ) -> anyhow::Result>; fn calculate_commission( &self, @@ -64,7 +63,7 @@ pub trait Account { last_px: Price, liquidity_side: LiquiditySide, use_quote_for_inverse: Option, - ) -> Result; + ) -> anyhow::Result; } pub mod base; diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs index d5e5282495aa..ce5e13097731 100644 --- a/nautilus_core/adapters/src/databento/common.rs +++ b/nautilus_core/adapters/src/databento/common.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::Result; use databento::historical::DateTimeRange; use nautilus_core::time::UnixNanos; use time::OffsetDateTime; @@ -21,7 +20,7 @@ use time::OffsetDateTime; pub const DATABENTO: &str = "DATABENTO"; pub const ALL_SYMBOLS: &str = "ALL_SYMBOLS"; -pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> Result { +pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result { Ok(DateTimeRange::from(( OffsetDateTime::from_unix_timestamp_nanos(i128::from(start))?, OffsetDateTime::from_unix_timestamp_nanos(i128::from(end))?, diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index e3e813c23a3f..c270139d2fa2 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -20,7 +20,6 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, bail, Result}; use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, time::UnixNanos}; use nautilus_model::{ data::{ @@ -94,29 +93,31 @@ pub fn parse_aggressor_side(c: c_char) -> AggressorSide { } } -pub fn parse_book_action(c: c_char) -> Result { +pub fn parse_book_action(c: c_char) -> anyhow::Result { match c as u8 as char { 'A' => Ok(BookAction::Add), 'C' => Ok(BookAction::Delete), 'F' => Ok(BookAction::Update), 'M' => Ok(BookAction::Update), 'R' => Ok(BookAction::Clear), - _ => bail!("Invalid `BookAction`, was '{c}'"), + _ => anyhow::bail!("Invalid `BookAction`, was '{c}'"), } } -pub fn parse_option_kind(c: c_char) -> Result { +pub fn parse_option_kind(c: c_char) -> anyhow::Result { match c as u8 as char { 'C' => Ok(OptionKind::Call), 'P' => Ok(OptionKind::Put), - _ => bail!("Invalid `OptionKind`, was '{c}'"), + _ => anyhow::bail!("Invalid `OptionKind`, was '{c}'"), } } -pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option)> { +pub fn parse_cfi_iso10926( + value: &str, +) -> anyhow::Result<(Option, Option)> { let chars: Vec = value.chars().collect(); if chars.len() < 3 { - bail!("Value string is too short"); + anyhow::bail!("Value string is too short"); } let cfi_category = chars[0]; @@ -145,21 +146,21 @@ pub fn parse_cfi_iso10926(value: &str) -> Result<(Option, Option Result { +pub fn decode_price(value: i64, precision: u8) -> anyhow::Result { match value { 0 | i64::MAX => Price::new(10f64.powi(-i32::from(precision)), precision), _ => Price::from_raw(value, precision), } } -pub fn decode_optional_price(value: i64, precision: u8) -> Result> { +pub fn decode_optional_price(value: i64, precision: u8) -> anyhow::Result> { match value { i64::MAX => Ok(None), _ => Ok(Some(Price::from_raw(value, precision)?)), } } -pub fn decode_optional_quantity_i32(value: i32) -> Result> { +pub fn decode_optional_quantity_i32(value: i32) -> anyhow::Result> { match value { i32::MAX => Ok(None), _ => Ok(Some(Quantity::new(f64::from(value), 0)?)), @@ -169,18 +170,18 @@ pub fn decode_optional_quantity_i32(value: i32) -> Result> { /// # Safety /// /// - Assumes `ptr` is a valid C string pointer. -pub unsafe fn raw_ptr_to_string(ptr: *const c_char) -> Result { +pub unsafe fn raw_ptr_to_string(ptr: *const c_char) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(ptr) }; - let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?; + let str_slice: &str = c_str.to_str().map_err(|e| anyhow::anyhow!(e))?; Ok(str_slice.to_owned()) } /// # Safety /// /// - Assumes `ptr` is a valid C string pointer. -pub unsafe fn raw_ptr_to_ustr(ptr: *const c_char) -> Result { +pub unsafe fn raw_ptr_to_ustr(ptr: *const c_char) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(ptr) }; - let str_slice: &str = c_str.to_str().map_err(|e| anyhow!(e))?; + let str_slice: &str = c_str.to_str().map_err(|e| anyhow::anyhow!(e))?; Ok(Ustr::from(str_slice)) } @@ -549,7 +550,7 @@ pub fn decode_bar_type( // ohlcv-1d BarType::new(instrument_id, BAR_SPEC_1D, AggregationSource::External) } - _ => bail!( + _ => anyhow::bail!( "`rtype` is not a supported bar aggregation, was {}", msg.hd.rtype ), @@ -576,7 +577,7 @@ pub fn decode_ts_event_adjustment(msg: &dbn::OhlcvMsg) -> anyhow::Result bail!( + _ => anyhow::bail!( "`rtype` is not a supported bar aggregation, was {}", msg.hd.rtype ), @@ -626,7 +627,7 @@ pub fn decode_record( (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"), + _ => anyhow::bail!("Invalid `MboMsg` parsing combination"), } } else if let Some(msg) = record.get::() { let ts_init = determine_timestamp(ts_init, msg.ts_recv); @@ -648,7 +649,7 @@ pub fn decode_record( let bar = decode_ohlcv_msg(msg, instrument_id, price_precision, ts_init)?; (Some(Data::Bar(bar)), None) } else { - bail!("DBN message type is not currently supported") + anyhow::bail!("DBN message type is not currently supported") }; Ok(result) @@ -692,9 +693,9 @@ pub fn decode_instrument_def_msg_v1( instrument_id, ts_init, )?)), - 'B' => bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), - _ => bail!( + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (BOND)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), + _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char ), @@ -732,9 +733,9 @@ pub fn decode_instrument_def_msg( instrument_id, ts_init, )?)), - 'B' => bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), - _ => bail!( + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (BOND)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), + _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char ), diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index e074f08b4f51..d0edc6559a7a 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -15,7 +15,6 @@ use std::{collections::HashMap, ffi::CStr}; -use anyhow::{anyhow, Result}; use databento::{ dbn::{PitSymbolMap, Record, SymbolIndex, VersionUpgradePolicy}, live::Subscription, @@ -103,7 +102,7 @@ impl DatabentoFeedHandler { } /// Run the feed handler to begin listening for commands and processing messages. - pub async fn run(&mut self) -> Result<()> { + pub async fn run(&mut self) -> anyhow::Result<()> { debug!("Running feed handler"); let clock = get_atomic_clock_realtime(); let mut symbol_map = PitSymbolMap::new(); @@ -129,7 +128,7 @@ impl DatabentoFeedHandler { Err(_) => { self.msg_tx.send(LiveMessage::Close).await?; self.cmd_rx.close(); - return Err(anyhow!("Timeout connecting to LSG")); + return Err(anyhow::anyhow!("Timeout connecting to LSG")); } }; @@ -198,7 +197,7 @@ impl DatabentoFeedHandler { Err(e) => { // Fail the session entirely for now. Consider refining // this strategy to handle specific errors more gracefully. - self.send_msg(LiveMessage::Error(anyhow!(e))).await; + self.send_msg(LiveMessage::Error(anyhow::anyhow!(e))).await; break; } }; @@ -374,7 +373,7 @@ fn handle_instrument_def_msg( msg: &dbn::InstrumentDefMsg, publisher_venue_map: &IndexMap, clock: &AtomicTime, -) -> Result { +) -> anyhow::Result { let c_str: &CStr = unsafe { CStr::from_ptr(msg.raw_symbol.as_ptr()) }; let raw_symbol: &str = c_str.to_str().map_err(to_pyvalue_err)?; diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 8df18278c8a5..635ddcd3e7ed 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -15,7 +15,6 @@ use std::{env, fs, path::PathBuf}; -use anyhow::Result; use dbn::{ compat::InstrumentDefMsgV1, decode::{dbn::Decoder, DbnMetadata, DecodeStream}, @@ -68,7 +67,7 @@ pub struct DatabentoDataLoader { } impl DatabentoDataLoader { - pub fn new(path: Option) -> Result { + pub fn new(path: Option) -> anyhow::Result { let mut loader = Self { publishers_map: IndexMap::new(), venue_dataset_map: IndexMap::new(), @@ -93,7 +92,7 @@ impl DatabentoDataLoader { } /// Load the publishers data from the file at the given `path`. - pub fn load_publishers(&mut self, path: PathBuf) -> Result<()> { + pub fn load_publishers(&mut self, path: PathBuf) -> anyhow::Result<()> { let file_content = fs::read_to_string(path)?; let publishers: Vec = serde_json::from_str(&file_content)?; @@ -139,7 +138,7 @@ impl DatabentoDataLoader { self.publisher_venue_map.get(&publisher_id) } - pub fn schema_from_file(&self, path: PathBuf) -> Result> { + pub fn schema_from_file(&self, path: PathBuf) -> anyhow::Result> { let decoder = Decoder::from_zstd_file(path)?; let metadata = decoder.metadata(); Ok(metadata.schema.map(|schema| schema.to_string())) @@ -148,7 +147,7 @@ impl DatabentoDataLoader { pub fn read_definition_records( &mut self, path: PathBuf, - ) -> Result> + '_> { + ) -> anyhow::Result> + '_> { let mut decoder = Decoder::from_zstd_file(path)?; decoder.set_upgrade_policy(dbn::VersionUpgradePolicy::Upgrade); let mut dbn_stream = decoder.decode_stream::(); @@ -188,7 +187,7 @@ impl DatabentoDataLoader { path: PathBuf, instrument_id: Option, include_trades: bool, - ) -> Result, Option)>> + '_> + ) -> anyhow::Result, Option)>> + '_> where T: dbn::Record + dbn::HasRType + 'static, { @@ -233,7 +232,7 @@ impl DatabentoDataLoader { &self, path: PathBuf, instrument_id: Option, - ) -> Result> + '_> + ) -> anyhow::Result> + '_> where T: dbn::Record + dbn::HasRType + 'static, { @@ -275,7 +274,7 @@ impl DatabentoDataLoader { &self, path: PathBuf, instrument_id: Option, - ) -> Result> + '_> + ) -> anyhow::Result> + '_> where T: dbn::Record + dbn::HasRType + 'static, { diff --git a/nautilus_core/adapters/src/databento/python/decode.rs b/nautilus_core/adapters/src/databento/python/decode.rs index 233d9effd911..0abbbfc6a506 100644 --- a/nautilus_core/adapters/src/databento/python/decode.rs +++ b/nautilus_core/adapters/src/databento/python/decode.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::bail; use nautilus_core::time::UnixNanos; use nautilus_model::{ data::{depth::OrderBookDepth10, trade::TradeTick}, @@ -72,7 +71,7 @@ pub fn py_decode_mbo_msg( if let Some(data) = data { Ok(data.into_py(py)) } else { - bail!("Error decoding MBO message") + anyhow::bail!("Error decoding MBO message") } } diff --git a/nautilus_core/adapters/src/databento/symbology.rs b/nautilus_core/adapters/src/databento/symbology.rs index 7225242d171b..8d65004d1905 100644 --- a/nautilus_core/adapters/src/databento/symbology.rs +++ b/nautilus_core/adapters/src/databento/symbology.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{anyhow, bail, Result}; use dbn::Record; use indexmap::IndexMap; use nautilus_model::identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}; @@ -24,12 +23,12 @@ pub fn decode_nautilus_instrument_id( record: &dbn::RecordRef, metadata: &dbn::Metadata, publisher_venue_map: &IndexMap, -) -> Result { +) -> anyhow::Result { let publisher = record.publisher().expect("Invalid `publisher` for record"); let publisher_id = publisher as PublisherId; let venue = publisher_venue_map .get(&publisher_id) - .ok_or_else(|| anyhow!("`Venue` not found for `publisher_id` {publisher_id}"))?; + .ok_or_else(|| anyhow::anyhow!("`Venue` not found for `publisher_id` {publisher_id}"))?; let instrument_id = get_nautilus_instrument_id_for_record(record, metadata, *venue)?; Ok(instrument_id) @@ -39,7 +38,7 @@ pub fn get_nautilus_instrument_id_for_record( record: &dbn::RecordRef, metadata: &dbn::Metadata, venue: Venue, -) -> Result { +) -> anyhow::Result { let (instrument_id, nanoseconds) = if let Some(msg) = record.get::() { (msg.hd.instrument_id, msg.ts_recv) } else if let Some(msg) = record.get::() { @@ -55,7 +54,7 @@ pub fn get_nautilus_instrument_id_for_record( } else if let Some(msg) = record.get::() { (msg.hd.instrument_id, msg.ts_recv) } else { - bail!("DBN message type is not currently supported") + anyhow::bail!("DBN message type is not currently supported") }; let duration = time::Duration::nanoseconds(nanoseconds as i64); @@ -66,7 +65,7 @@ pub fn get_nautilus_instrument_id_for_record( let symbol_map = metadata.symbol_map_for_date(date)?; let raw_symbol = symbol_map .get(instrument_id) - .ok_or_else(|| anyhow!("No raw symbol found for {instrument_id}"))?; + .ok_or_else(|| anyhow::anyhow!("No raw symbol found for {instrument_id}"))?; let symbol = Symbol::from_str_unchecked(raw_symbol); diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 4a33565efeb9..0af19e184a4b 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{bail, Result}; - const FAILED: &str = "Condition failed:"; /// Validates the content of a string `s`. @@ -24,76 +22,78 @@ const FAILED: &str = "Condition failed:"; /// - If `s` is an empty string. /// - If `s` consists solely of whitespace characters. /// - If `s` contains one or more non-ASCII characters. -pub fn check_valid_string(s: &str, desc: &str) -> Result<()> { +pub fn check_valid_string(s: &str, desc: &str) -> anyhow::Result<()> { if s.is_empty() { - bail!("{FAILED} invalid string for {desc}, was empty") + anyhow::bail!("{FAILED} invalid string for {desc}, was empty") } else if s.chars().all(char::is_whitespace) { - bail!("{FAILED} invalid string for {desc}, was all whitespace",) + anyhow::bail!("{FAILED} invalid string for {desc}, was all whitespace",) } else if !s.is_ascii() { - bail!("{FAILED} invalid string for {desc} contained a non-ASCII char, was '{s}'",) + anyhow::bail!("{FAILED} invalid string for {desc} contained a non-ASCII char, was '{s}'",) } else { Ok(()) } } /// Validates that the string `s` contains the pattern `pat`. -pub fn check_string_contains(s: &str, pat: &str, desc: &str) -> Result<()> { +pub fn check_string_contains(s: &str, pat: &str, desc: &str) -> anyhow::Result<()> { if !s.contains(pat) { - bail!("{FAILED} invalid string for {desc} did not contain '{pat}', was '{s}'") + anyhow::bail!("{FAILED} invalid string for {desc} did not contain '{pat}', was '{s}'") } Ok(()) } /// Validates that `u8` values are equal. -pub fn check_u8_equal(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> Result<()> { +pub fn check_u8_equal(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> { if lhs != rhs { - bail!("{FAILED} '{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}") + anyhow::bail!( + "{FAILED} '{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}" + ) } Ok(()) } /// Validates that the `u8` value is in the inclusive range [`l`, `r`]. -pub fn check_u8_in_range_inclusive(value: u8, l: u8, r: u8, desc: &str) -> Result<()> { +pub fn check_u8_in_range_inclusive(value: u8, l: u8, r: u8, desc: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid u8 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u8 for {desc} not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `u64` value is in the inclusive range [`l`, `r`]. -pub fn check_u64_in_range_inclusive(value: u64, l: u64, r: u64, desc: &str) -> Result<()> { +pub fn check_u64_in_range_inclusive(value: u64, l: u64, r: u64, desc: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid u64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u64 for {desc} not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `i64` value is in the inclusive range [`l`, `r`]. -pub fn check_i64_in_range_inclusive(value: i64, l: i64, r: i64, desc: &str) -> Result<()> { +pub fn check_i64_in_range_inclusive(value: i64, l: i64, r: i64, desc: &str) -> anyhow::Result<()> { if value < l || value > r { - bail!("{FAILED} invalid i64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid i64 for {desc} not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `f64` value is in the inclusive range [`l`, `r`]. -pub fn check_f64_in_range_inclusive(value: f64, l: f64, r: f64, desc: &str) -> Result<()> { +pub fn check_f64_in_range_inclusive(value: f64, l: f64, r: f64, desc: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { - bail!("{FAILED} invalid f64 for {desc}, was {value}") + anyhow::bail!("{FAILED} invalid f64 for {desc}, was {value}") } if value < l || value > r { - bail!("{FAILED} invalid f64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid f64 for {desc} not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `f64` value is non-negative. -pub fn check_f64_non_negative(value: f64, desc: &str) -> Result<()> { +pub fn check_f64_non_negative(value: f64, desc: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { - bail!("{FAILED} invalid f64 for {desc}, was {value}") + anyhow::bail!("{FAILED} invalid f64 for {desc}, was {value}") } if value < 0.0 { - bail!("{FAILED} invalid f64 for {desc} negative, was {value}") + anyhow::bail!("{FAILED} invalid f64 for {desc} negative, was {value}") } Ok(()) } diff --git a/nautilus_core/core/src/datetime.rs b/nautilus_core/core/src/datetime.rs index 7aae4af1db79..6995ecf1461c 100644 --- a/nautilus_core/core/src/datetime.rs +++ b/nautilus_core/core/src/datetime.rs @@ -15,7 +15,6 @@ use std::time::{Duration, UNIX_EPOCH}; -use anyhow::{anyhow, Result}; use chrono::{ prelude::{DateTime, Utc}, Datelike, NaiveDate, SecondsFormat, TimeDelta, Weekday, @@ -93,8 +92,9 @@ pub fn unix_nanos_to_iso8601(timestamp_ns: u64) -> String { dt.to_rfc3339_opts(SecondsFormat::Nanos, true) } -pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> Result { - let date = NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow!("Invalid date"))?; +pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> anyhow::Result { + let date = + NaiveDate::from_ymd_opt(year, month, day).ok_or_else(|| anyhow::anyhow!("Invalid date"))?; let current_weekday = date.weekday().number_from_monday(); // Calculate the offset in days for closest weekday (Mon-Fri) @@ -110,19 +110,19 @@ pub fn last_weekday_nanos(year: i32, month: u32, day: u32) -> Result // Convert to UNIX nanoseconds let unix_timestamp_ns = last_closest .and_hms_nano_opt(0, 0, 0, 0) - .ok_or_else(|| anyhow!("Failed `and_hms_nano_opt`"))?; + .ok_or_else(|| anyhow::anyhow!("Failed `and_hms_nano_opt`"))?; Ok(unix_timestamp_ns .and_utc() .timestamp_nanos_opt() - .ok_or_else(|| anyhow!("Failed `timestamp_nanos_opt`"))? as UnixNanos) + .ok_or_else(|| anyhow::anyhow!("Failed `timestamp_nanos_opt`"))? as UnixNanos) } -pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> Result { +pub fn is_within_last_24_hours(timestamp_ns: UnixNanos) -> anyhow::Result { let seconds = timestamp_ns / NANOSECONDS_IN_SECOND; let nanoseconds = (timestamp_ns % NANOSECONDS_IN_SECOND) as u32; let timestamp = DateTime::from_timestamp(seconds as i64, nanoseconds) - .ok_or_else(|| anyhow!("Invalid timestamp {timestamp_ns}"))?; + .ok_or_else(|| anyhow::anyhow!("Invalid timestamp {timestamp_ns}"))?; let now = Utc::now(); Ok(now.signed_duration_since(timestamp) <= TimeDelta::try_days(1).unwrap()) diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index d571c75a2e78..1910d7d91a51 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{anyhow, Result}; - /// Returns the decimal precision inferred from the given string. #[must_use] pub fn precision_from_str(s: &str) -> u8 { @@ -30,7 +28,7 @@ pub fn precision_from_str(s: &str) -> u8 { } /// Returns a usize from the given bytes. -pub fn bytes_to_usize(bytes: &[u8]) -> Result { +pub fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result { // Check bytes width if bytes.len() >= std::mem::size_of::() { let mut buffer = [0u8; std::mem::size_of::()]; @@ -38,7 +36,7 @@ pub fn bytes_to_usize(bytes: &[u8]) -> Result { Ok(usize::from_le_bytes(buffer)) } else { - Err(anyhow!("Not enough bytes to represent a `usize`")) + Err(anyhow::anyhow!("Not enough bytes to represent a `usize`")) } } diff --git a/nautilus_core/indicators/src/average/ama.rs b/nautilus_core/indicators/src/average/ama.rs index cbdd17a705f0..52246dd49491 100644 --- a/nautilus_core/indicators/src/average/ama.rs +++ b/nautilus_core/indicators/src/average/ama.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -110,7 +109,7 @@ impl AdaptiveMovingAverage { period_fast: usize, period_slow: usize, price_type: Option, - ) -> Result { + ) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/dema.rs b/nautilus_core/indicators/src/average/dema.rs index 560feba9a1c6..3c465db7f045 100644 --- a/nautilus_core/indicators/src/average/dema.rs +++ b/nautilus_core/indicators/src/average/dema.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -88,7 +87,7 @@ impl Indicator for DoubleExponentialMovingAverage { } impl DoubleExponentialMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/average/ema.rs b/nautilus_core/indicators/src/average/ema.rs index 9bb4ba54f6f6..63829e68036b 100644 --- a/nautilus_core/indicators/src/average/ema.rs +++ b/nautilus_core/indicators/src/average/ema.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -79,7 +78,7 @@ impl Indicator for ExponentialMovingAverage { } impl ExponentialMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/hma.rs b/nautilus_core/indicators/src/average/hma.rs index dab724292dd8..cc025cf6e1ef 100644 --- a/nautilus_core/indicators/src/average/hma.rs +++ b/nautilus_core/indicators/src/average/hma.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -97,7 +96,7 @@ fn _get_weights(size: usize) -> Vec { } impl HullMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { let period_halved = period / 2; let period_sqrt = (period as f64).sqrt() as usize; diff --git a/nautilus_core/indicators/src/average/rma.rs b/nautilus_core/indicators/src/average/rma.rs index 46c8b9cc59e1..fc43d102a68c 100644 --- a/nautilus_core/indicators/src/average/rma.rs +++ b/nautilus_core/indicators/src/average/rma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -79,7 +78,7 @@ impl Indicator for WilderMovingAverage { } impl WilderMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. // The Wilder Moving Average is The Wilder's Moving Average is simply diff --git a/nautilus_core/indicators/src/average/sma.rs b/nautilus_core/indicators/src/average/sma.rs index fcaff18f75c3..705ace65122b 100644 --- a/nautilus_core/indicators/src/average/sma.rs +++ b/nautilus_core/indicators/src/average/sma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -78,7 +77,7 @@ impl Indicator for SimpleMovingAverage { } impl SimpleMovingAverage { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/average/vidya.rs b/nautilus_core/indicators/src/average/vidya.rs index 02e0cd339f4b..eb780edb1520 100644 --- a/nautilus_core/indicators/src/average/vidya.rs +++ b/nautilus_core/indicators/src/average/vidya.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -91,7 +90,7 @@ impl VariableIndexDynamicAverage { period: usize, price_type: Option, cmo_ma_type: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs index aef7763de342..b2bb5ee94531 100644 --- a/nautilus_core/indicators/src/average/wma.rs +++ b/nautilus_core/indicators/src/average/wma.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -53,7 +52,11 @@ impl Display for WeightedMovingAverage { } impl WeightedMovingAverage { - pub fn new(period: usize, weights: Vec, price_type: Option) -> Result { + pub fn new( + period: usize, + weights: Vec, + price_type: Option, + ) -> anyhow::Result { if weights.len() != period { return Err(anyhow::anyhow!("Weights length must be equal to period")); } diff --git a/nautilus_core/indicators/src/book/imbalance.rs b/nautilus_core/indicators/src/book/imbalance.rs index 9f5c85f670dd..46311f0455ee 100644 --- a/nautilus_core/indicators/src/book/imbalance.rs +++ b/nautilus_core/indicators/src/book/imbalance.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ orderbook::{book_mbo::OrderBookMbo, book_mbp::OrderBookMbp}, types::quantity::Quantity, @@ -72,7 +71,7 @@ impl Indicator for BookImbalanceRatio { } impl BookImbalanceRatio { - pub fn new() -> Result { + pub fn new() -> anyhow::Result { // Inputs don't require validation, however we return a `Result` // to standardize with other indicators which do need validation. Ok(Self { diff --git a/nautilus_core/indicators/src/momentum/aroon.rs b/nautilus_core/indicators/src/momentum/aroon.rs index ee3c464d9d44..db0ce27d0779 100644 --- a/nautilus_core/indicators/src/momentum/aroon.rs +++ b/nautilus_core/indicators/src/momentum/aroon.rs @@ -18,7 +18,6 @@ use std::{ fmt::{Debug, Display}, }; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -92,7 +91,7 @@ impl Indicator for AroonOscillator { } impl AroonOscillator { - pub fn new(period: usize) -> Result { + pub fn new(period: usize) -> anyhow::Result { Ok(Self { period, high_inputs: VecDeque::with_capacity(period), diff --git a/nautilus_core/indicators/src/momentum/cmo.rs b/nautilus_core/indicators/src/momentum/cmo.rs index b84a0713cbff..d94dc6f57217 100644 --- a/nautilus_core/indicators/src/momentum/cmo.rs +++ b/nautilus_core/indicators/src/momentum/cmo.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::data::{bar::Bar, quote::QuoteTick, trade::TradeTick}; use crate::{ @@ -82,7 +81,7 @@ impl Indicator for ChandeMomentumOscillator { } impl ChandeMomentumOscillator { - pub fn new(period: usize, ma_type: Option) -> Result { + pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Wilder), diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs index 2ad93d9723a8..ea0f2c5ea9b8 100644 --- a/nautilus_core/indicators/src/momentum/rsi.rs +++ b/nautilus_core/indicators/src/momentum/rsi.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display}; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -87,7 +86,7 @@ impl Indicator for RelativeStrengthIndex { } impl RelativeStrengthIndex { - pub fn new(period: usize, ma_type: Option) -> Result { + pub fn new(period: usize, ma_type: Option) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Exponential), diff --git a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs index 3c994ab55ef9..0cda086051d4 100644 --- a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs +++ b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -80,7 +79,7 @@ impl Indicator for EfficiencyRatio { } impl EfficiencyRatio { - pub fn new(period: usize, price_type: Option) -> Result { + pub fn new(period: usize, price_type: Option) -> anyhow::Result { Ok(Self { period, price_type: price_type.unwrap_or(PriceType::Last), diff --git a/nautilus_core/indicators/src/volatility/atr.rs b/nautilus_core/indicators/src/volatility/atr.rs index 94dfd38a93fd..d41014b7850c 100644 --- a/nautilus_core/indicators/src/volatility/atr.rs +++ b/nautilus_core/indicators/src/volatility/atr.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display}; -use anyhow::Result; use nautilus_model::data::bar::Bar; use crate::{ @@ -89,7 +88,7 @@ impl AverageTrueRange { ma_type: Option, use_previous: Option, value_floor: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { period, ma_type: ma_type.unwrap_or(MovingAverageType::Simple), diff --git a/nautilus_core/infrastructure/src/cache.rs b/nautilus_core/infrastructure/src/cache.rs index 4c0f7c985ef9..452b4d1ec00b 100644 --- a/nautilus_core/infrastructure/src/cache.rs +++ b/nautilus_core/infrastructure/src/cache.rs @@ -15,7 +15,6 @@ use std::{collections::HashMap, sync::mpsc::Receiver}; -use anyhow::Result; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; @@ -61,13 +60,13 @@ pub trait CacheDatabase { trader_id: TraderId, instance_id: UUID4, config: HashMap, - ) -> Result; - fn flushdb(&mut self) -> Result<()>; - fn keys(&mut self, pattern: &str) -> Result>; - fn read(&mut self, key: &str) -> Result>>; - fn insert(&mut self, key: String, payload: Option>>) -> Result<()>; - fn update(&mut self, key: String, payload: Option>>) -> Result<()>; - fn delete(&mut self, key: String, payload: Option>>) -> Result<()>; + ) -> anyhow::Result; + fn flushdb(&mut self) -> anyhow::Result<()>; + fn keys(&mut self, pattern: &str) -> anyhow::Result>; + fn read(&mut self, key: &str) -> anyhow::Result>>; + fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; fn handle_messages( rx: Receiver, trader_key: String, diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 91a174a2f59e..8ee0c02eee94 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -20,7 +20,6 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{anyhow, bail, Result}; use nautilus_common::redis::{get_buffer_interval, get_redis_url, get_timeout_duration}; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; @@ -82,7 +81,7 @@ impl CacheDatabase for RedisCacheDatabase { trader_id: TraderId, instance_id: UUID4, config: HashMap, - ) -> Result { + ) -> anyhow::Result { debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); let redis_url = get_redis_url(&config); let default_timeout = 20; @@ -110,21 +109,21 @@ impl CacheDatabase for RedisCacheDatabase { }) } - fn flushdb(&mut self) -> Result<()> { + fn flushdb(&mut self) -> anyhow::Result<()> { match redis::cmd(FLUSHDB).query::<()>(&mut self.conn) { Ok(_) => Ok(()), Err(e) => Err(e.into()), } } - fn keys(&mut self, pattern: &str) -> Result> { + fn keys(&mut self, pattern: &str) -> anyhow::Result> { match self.conn.keys(pattern) { Ok(keys) => Ok(keys), Err(e) => Err(e.into()), } } - fn read(&mut self, key: &str) -> Result>> { + fn read(&mut self, key: &str) -> anyhow::Result>> { let collection = get_collection_key(key)?; let key = format!("{}{DELIMITER}{}", self.trader_key, key); @@ -139,31 +138,31 @@ impl CacheDatabase for RedisCacheDatabase { POSITIONS => read_list(&mut self.conn, &key), ACTORS => read_string(&mut self.conn, &key), STRATEGIES => read_string(&mut self.conn, &key), - _ => bail!("Unsupported operation: `read` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `read` for collection '{collection}'"), } } - fn insert(&mut self, key: String, payload: Option>>) -> Result<()> { + fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Insert, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } - fn update(&mut self, key: String, payload: Option>>) -> Result<()> { + fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Update, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } - fn delete(&mut self, key: String, payload: Option>>) -> Result<()> { + fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()> { let op = DatabaseCommand::new(DatabaseOperation::Delete, key, payload); match self.tx.send(op) { Ok(_) => Ok(()), - Err(e) => bail!("{CHANNEL_TX_FAILED}: {e}"), + Err(e) => anyhow::bail!("{CHANNEL_TX_FAILED}: {e}"), } } @@ -274,7 +273,7 @@ fn drain_buffer(conn: &mut Connection, trader_key: &str, buffer: &mut VecDeque Result>> { +fn read_index(conn: &mut Connection, key: &str) -> anyhow::Result>> { let index_key = get_index_key(key)?; match index_key { INDEX_ORDER_IDS => read_set(conn, key), @@ -288,11 +287,11 @@ fn read_index(conn: &mut Connection, key: &str) -> Result>> { INDEX_POSITIONS => read_set(conn, key), INDEX_POSITIONS_OPEN => read_set(conn, key), INDEX_POSITIONS_CLOSED => read_set(conn, key), - _ => bail!("Index unknown '{index_key}' on read"), + _ => anyhow::bail!("Index unknown '{index_key}' on read"), } } -fn read_string(conn: &mut Connection, key: &str) -> Result>> { +fn read_string(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec = conn.get(key)?; if result.is_empty() { @@ -302,25 +301,30 @@ fn read_string(conn: &mut Connection, key: &str) -> Result>> { } } -fn read_set(conn: &mut Connection, key: &str) -> Result>> { +fn read_set(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec> = conn.smembers(key)?; Ok(result) } -fn read_hset(conn: &mut Connection, key: &str) -> Result>> { +fn read_hset(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: HashMap = conn.hgetall(key)?; let json = serde_json::to_string(&result)?; Ok(vec![json.into_bytes()]) } -fn read_list(conn: &mut Connection, key: &str) -> Result>> { +fn read_list(conn: &mut Connection, key: &str) -> anyhow::Result>> { let result: Vec> = conn.lrange(key, 0, -1)?; Ok(result) } -fn insert(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) -> Result<()> { +fn insert( + pipe: &mut Pipeline, + collection: &str, + key: &str, + value: Vec<&[u8]>, +) -> anyhow::Result<()> { if value.is_empty() { - bail!("Empty `payload` for `insert`") + anyhow::bail!("Empty `payload` for `insert`") } match collection { @@ -369,11 +373,11 @@ fn insert(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) - insert_string(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported operation: `insert` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `insert` for collection '{collection}'"), } } -fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> Result<()> { +fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> anyhow::Result<()> { let index_key = get_index_key(key)?; match index_key { INDEX_ORDER_IDS => { @@ -420,7 +424,7 @@ fn insert_index(pipe: &mut Pipeline, key: &str, value: &[&[u8]]) -> Result<()> { insert_set(pipe, key, value[0]); Ok(()) } - _ => bail!("Index unknown '{index_key}' on insert"), + _ => anyhow::bail!("Index unknown '{index_key}' on insert"), } } @@ -440,9 +444,14 @@ fn insert_list(pipe: &mut Pipeline, key: &str, value: &[u8]) { pipe.rpush(key, value); } -fn update(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) -> Result<()> { +fn update( + pipe: &mut Pipeline, + collection: &str, + key: &str, + value: Vec<&[u8]>, +) -> anyhow::Result<()> { if value.is_empty() { - bail!("Empty `payload` for `update`") + anyhow::bail!("Empty `payload` for `update`") } match collection { @@ -458,7 +467,7 @@ fn update(pipe: &mut Pipeline, collection: &str, key: &str, value: Vec<&[u8]>) - update_list(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported operation: `update` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `update` for collection '{collection}'"), } } @@ -471,7 +480,7 @@ fn delete( collection: &str, key: &str, value: Option>, -) -> Result<()> { +) -> anyhow::Result<()> { match collection { INDEX => remove_index(pipe, key, value), ACTORS => { @@ -482,12 +491,12 @@ fn delete( delete_string(pipe, key); Ok(()) } - _ => bail!("Unsupported operation: `delete` for collection '{collection}'"), + _ => anyhow::bail!("Unsupported operation: `delete` for collection '{collection}'"), } } -fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> Result<()> { - let value = value.ok_or_else(|| anyhow!("Empty `payload` for `delete` '{key}'"))?; +fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> anyhow::Result<()> { + let value = value.ok_or_else(|| anyhow::anyhow!("Empty `payload` for `delete` '{key}'"))?; let index_key = get_index_key(key)?; match index_key { @@ -515,7 +524,7 @@ fn remove_index(pipe: &mut Pipeline, key: &str, value: Option>) -> Re remove_from_set(pipe, key, value[0]); Ok(()) } - _ => bail!("Unsupported index operation: remove from '{index_key}'"), + _ => anyhow::bail!("Unsupported index operation: remove from '{index_key}'"), } } @@ -548,16 +557,20 @@ fn get_trader_key( key } -fn get_collection_key(key: &str) -> Result<&str> { +fn get_collection_key(key: &str) -> anyhow::Result<&str> { key.split_once(DELIMITER) .map(|(collection, _)| collection) - .ok_or_else(|| anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}")) + .ok_or_else(|| { + anyhow::anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}") + }) } -fn get_index_key(key: &str) -> Result<&str> { +fn get_index_key(key: &str) -> anyhow::Result<&str> { key.split_once(DELIMITER) .map(|(_, index_key)| index_key) - .ok_or_else(|| anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}")) + .ok_or_else(|| { + anyhow::anyhow!("Invalid `key`, missing a '{DELIMITER}' delimiter, was {key}") + }) } // This function can be used when we handle cache serialization in Rust @@ -575,13 +588,13 @@ fn get_encoding(config: &HashMap) -> String { fn deserialize_payload( encoding: &str, payload: &[u8], -) -> Result> { +) -> anyhow::Result> { match encoding { "msgpack" => rmp_serde::from_slice(payload) - .map_err(|e| anyhow!("Failed to deserialize msgpack `payload`: {e}")), + .map_err(|e| anyhow::anyhow!("Failed to deserialize msgpack `payload`: {e}")), "json" => serde_json::from_slice(payload) - .map_err(|e| anyhow!("Failed to deserialize json `payload`: {e}")), - _ => Err(anyhow!("Unsupported encoding: {encoding}")), + .map_err(|e| anyhow::anyhow!("Failed to deserialize json `payload`: {e}")), + _ => Err(anyhow::anyhow!("Unsupported encoding: {encoding}")), } } diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index d37d1daa8a62..81a1b7664795 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -20,7 +20,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use indexmap::IndexMap; use nautilus_core::{correctness::check_u8_equal, serialization::Serializable, time::UnixNanos}; use serde::{Deserialize, Serialize}; @@ -66,7 +65,7 @@ impl QuoteTick { ask_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { check_u8_equal( bid_price.precision, ask_price.precision, diff --git a/nautilus_core/model/src/events/account/state.rs b/nautilus_core/model/src/events/account/state.rs index 71be35979981..1ec3ef73d5bb 100644 --- a/nautilus_core/model/src/events/account/state.rs +++ b/nautilus_core/model/src/events/account/state.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -58,7 +57,7 @@ impl AccountState { ts_event: UnixNanos, ts_init: UnixNanos, base_currency: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { account_id, account_type, diff --git a/nautilus_core/model/src/events/order/accepted.rs b/nautilus_core/model/src/events/order/accepted.rs index 70122f5bed3b..f2a0663b297f 100644 --- a/nautilus_core/model/src/events/order/accepted.rs +++ b/nautilus_core/model/src/events/order/accepted.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderAccepted { ts_event: UnixNanos, ts_init: UnixNanos, reconciliation: bool, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/cancel_rejected.rs b/nautilus_core/model/src/events/order/cancel_rejected.rs index 6d413617e491..17e073c126f1 100644 --- a/nautilus_core/model/src/events/order/cancel_rejected.rs +++ b/nautilus_core/model/src/events/order/cancel_rejected.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -62,7 +61,7 @@ impl OrderCancelRejected { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/canceled.rs b/nautilus_core/model/src/events/order/canceled.rs index ce64080d2b1d..ad0d8ca6bdbc 100644 --- a/nautilus_core/model/src/events/order/canceled.rs +++ b/nautilus_core/model/src/events/order/canceled.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderCanceled { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/denied.rs b/nautilus_core/model/src/events/order/denied.rs index 479587c0f08d..811aee711e2d 100644 --- a/nautilus_core/model/src/events/order/denied.rs +++ b/nautilus_core/model/src/events/order/denied.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -56,7 +55,7 @@ impl OrderDenied { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/emulated.rs b/nautilus_core/model/src/events/order/emulated.rs index 276f3d287035..860e90d060f0 100644 --- a/nautilus_core/model/src/events/order/emulated.rs +++ b/nautilus_core/model/src/events/order/emulated.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -53,7 +52,7 @@ impl OrderEmulated { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/expired.rs b/nautilus_core/model/src/events/order/expired.rs index db5583959606..b6f264c53fb4 100644 --- a/nautilus_core/model/src/events/order/expired.rs +++ b/nautilus_core/model/src/events/order/expired.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderExpired { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/filled.rs b/nautilus_core/model/src/events/order/filled.rs index 19f6aee315cb..3df1d0d727f2 100644 --- a/nautilus_core/model/src/events/order/filled.rs +++ b/nautilus_core/model/src/events/order/filled.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -82,7 +81,7 @@ impl OrderFilled { reconciliation: bool, position_id: Option, commission: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/initialized.rs b/nautilus_core/model/src/events/order/initialized.rs index 4b31dfea83f1..7d8a442321ed 100644 --- a/nautilus_core/model/src/events/order/initialized.rs +++ b/nautilus_core/model/src/events/order/initialized.rs @@ -18,7 +18,6 @@ use std::{ fmt::{Display, Formatter}, }; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -154,7 +153,7 @@ impl OrderInitialized { exec_algorithm_params: Option>, exec_spawn_id: Option, tags: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/modify_rejected.rs b/nautilus_core/model/src/events/order/modify_rejected.rs index 384b7de4c9e0..cd671e25b37a 100644 --- a/nautilus_core/model/src/events/order/modify_rejected.rs +++ b/nautilus_core/model/src/events/order/modify_rejected.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -62,7 +61,7 @@ impl OrderModifyRejected { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/pending_cancel.rs b/nautilus_core/model/src/events/order/pending_cancel.rs index c6a792697c4c..f483bcf47d33 100644 --- a/nautilus_core/model/src/events/order/pending_cancel.rs +++ b/nautilus_core/model/src/events/order/pending_cancel.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderPendingCancel { ts_init: UnixNanos, reconciliation: bool, venue_order_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/pending_update.rs b/nautilus_core/model/src/events/order/pending_update.rs index 9bd4a45561d8..9377710019c2 100644 --- a/nautilus_core/model/src/events/order/pending_update.rs +++ b/nautilus_core/model/src/events/order/pending_update.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderPendingUpdate { ts_init: UnixNanos, reconciliation: bool, venue_order_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/rejected.rs b/nautilus_core/model/src/events/order/rejected.rs index 146e804a52be..ed8e4073f697 100644 --- a/nautilus_core/model/src/events/order/rejected.rs +++ b/nautilus_core/model/src/events/order/rejected.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -60,7 +59,7 @@ impl OrderRejected { ts_event: UnixNanos, ts_init: UnixNanos, reconciliation: bool, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/released.rs b/nautilus_core/model/src/events/order/released.rs index d35d9169088d..762404e068d8 100644 --- a/nautilus_core/model/src/events/order/released.rs +++ b/nautilus_core/model/src/events/order/released.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -58,7 +57,7 @@ impl OrderReleased { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/submitted.rs b/nautilus_core/model/src/events/order/submitted.rs index ffa7eacc9188..6d4c40ba16ed 100644 --- a/nautilus_core/model/src/events/order/submitted.rs +++ b/nautilus_core/model/src/events/order/submitted.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -55,7 +54,7 @@ impl OrderSubmitted { event_id: UUID4, ts_event: UnixNanos, ts_init: UnixNanos, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/triggered.rs b/nautilus_core/model/src/events/order/triggered.rs index 26e3e1bd6ac4..873adac70c3f 100644 --- a/nautilus_core/model/src/events/order/triggered.rs +++ b/nautilus_core/model/src/events/order/triggered.rs @@ -15,7 +15,6 @@ use std::fmt::Display; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -59,7 +58,7 @@ impl OrderTriggered { reconciliation: bool, venue_order_id: Option, account_id: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/events/order/updated.rs b/nautilus_core/model/src/events/order/updated.rs index 96a3393d7480..c33c8025d258 100644 --- a/nautilus_core/model/src/events/order/updated.rs +++ b/nautilus_core/model/src/events/order/updated.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use derive_builder::Builder; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ impl OrderUpdated { account_id: Option, price: Option, trigger_price: Option, - ) -> Result { + ) -> anyhow::Result { Ok(Self { trader_id, strategy_id, diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index cb6091dd0c69..524c39a77fb3 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; @@ -41,7 +40,7 @@ pub struct AccountId { } impl AccountId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`accountid` value")?; check_string_contains(s, "-", "`AccountId` value")?; diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index 91567cea588f..5d34bb3d52f3 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct ClientId { } impl ClientId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`ClientId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index e484b64d3c05..fa7cb744664d 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct ClientOrderId { } impl ClientOrderId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`ClientOrderId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index ced0a8c39b56..c6710272c23b 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct ComponentId { } impl ComponentId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`ComponentId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs index 7380d8edfc87..3250ba77869a 100644 --- a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs +++ b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct ExecAlgorithmId { } impl ExecAlgorithmId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`ExecAlgorithmId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/instrument_id.rs b/nautilus_core/model/src/identifiers/instrument_id.rs index 669feebf7f9f..ef154a93310a 100644 --- a/nautilus_core/model/src/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/identifiers/instrument_id.rs @@ -19,7 +19,6 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Deserializer, Serialize}; use crate::identifiers::{symbol::Symbol, venue::Venue}; @@ -55,16 +54,16 @@ impl InstrumentId { impl FromStr for InstrumentId { type Err = anyhow::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> anyhow::Result { match s.rsplit_once('.') { Some((symbol_part, venue_part)) => Ok(Self { symbol: Symbol::new(symbol_part) - .map_err(|e| anyhow!(err_message(s, e.to_string())))?, + .map_err(|e| anyhow::anyhow!(err_message(s, e.to_string())))?, venue: Venue::new(venue_part) - .map_err(|e| anyhow!(err_message(s, e.to_string())))?, + .map_err(|e| anyhow::anyhow!(err_message(s, e.to_string())))?, }), None => { - bail!(err_message( + anyhow::bail!(err_message( s, "Missing '.' separator between symbol and venue components".to_string() )) diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index 856979e87dfd..442bc613fde1 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct OrderListId { } impl OrderListId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`OrderListId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 38f72a8ac070..5347c3f02132 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct PositionId { } impl PositionId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`PositionId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index d933fd3dece7..1e2cd9a6cb3d 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display, Formatter}; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; @@ -41,7 +40,7 @@ pub struct StrategyId { } impl StrategyId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`StrategyId` value")?; if s != "EXTERNAL" { check_string_contains(s, "-", "`StrategyId` value")?; diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index 475fa3a878d2..45034110a167 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct Symbol { } impl Symbol { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`Symbol` value")?; Ok(Self { diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 06d1262c2bcb..7362503b8dfc 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -19,7 +19,6 @@ use std::{ hash::Hash, }; -use anyhow::{bail, Result}; use nautilus_core::correctness::check_valid_string; use serde::{Deserialize, Deserializer, Serialize}; @@ -42,20 +41,20 @@ pub struct TradeId { } impl TradeId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { let cstr = CString::new(s).expect("`CString` conversion failed"); Self::from_cstr(cstr) } - pub fn from_cstr(cstr: CString) -> Result { + pub fn from_cstr(cstr: CString) -> anyhow::Result { check_valid_string(cstr.to_str()?, "`TradeId` value")?; // TODO: Temporarily make this 65 to accommodate Betfair trade IDs // TODO: Extract this to single function let bytes = cstr.as_bytes_with_nul(); if bytes.len() > 37 { - bail!("Condition failed: value exceeds maximum trade ID length of 36"); + anyhow::bail!("Condition failed: value exceeds maximum trade ID length of 36"); } let mut value = [0; 37]; value[..bytes.len()].copy_from_slice(bytes); diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 226108fdf24d..285685928a49 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -15,7 +15,6 @@ use std::fmt::{Debug, Display, Formatter}; -use anyhow::Result; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; @@ -41,7 +40,7 @@ pub struct TraderId { } impl TraderId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`TraderId` value")?; check_string_contains(s, "-", "`TraderId` value")?; diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index c1cc32b1fc3c..ed4da552399d 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::{anyhow, Result}; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -39,7 +38,7 @@ pub struct Venue { } impl Venue { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`Venue` value")?; Ok(Self { @@ -54,14 +53,14 @@ impl Venue { } } - pub fn from_code(code: &str) -> Result { + pub fn from_code(code: &str) -> anyhow::Result { let map_guard = VENUE_MAP .lock() - .map_err(|e| anyhow!("Failed to acquire lock on `VENUE_MAP`: {e}"))?; + .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `VENUE_MAP`: {e}"))?; map_guard .get(code) .copied() - .ok_or_else(|| anyhow!("Unknown venue code: {code}")) + .ok_or_else(|| anyhow::anyhow!("Unknown venue code: {code}")) } #[must_use] diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index 17105d0568ae..70badcd00c0e 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -18,7 +18,6 @@ use std::{ hash::Hash, }; -use anyhow::Result; use nautilus_core::correctness::check_valid_string; use ustr::Ustr; @@ -35,7 +34,7 @@ pub struct VenueOrderId { } impl VenueOrderId { - pub fn new(s: &str) -> Result { + pub fn new(s: &str) -> anyhow::Result { check_valid_string(s, "`VenueOrderId` value")?; Ok(Self { diff --git a/nautilus_core/model/src/instruments/mod.rs b/nautilus_core/model/src/instruments/mod.rs index a5ab72b37d3c..813e5c47d63a 100644 --- a/nautilus_core/model/src/instruments/mod.rs +++ b/nautilus_core/model/src/instruments/mod.rs @@ -27,7 +27,6 @@ pub mod synthetic; #[cfg(feature = "stubs")] pub mod stubs; -use anyhow::Result; use nautilus_core::time::UnixNanos; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -99,12 +98,12 @@ pub trait Instrument: Any + 'static + Send { fn ts_init(&self) -> UnixNanos; /// Creates a new price from the given `value` with the correct price precision for the instrument. - fn make_price(&self, value: f64) -> Result { + fn make_price(&self, value: f64) -> anyhow::Result { Price::new(value, self.price_precision()) } /// Creates a new quantity from the given `value` with the correct size precision for the instrument. - fn make_qty(&self, value: f64) -> Result { + fn make_qty(&self, value: f64) -> anyhow::Result { Quantity::new(value, self.size_precision()) } diff --git a/nautilus_core/model/src/instruments/synthetic.rs b/nautilus_core/model/src/instruments/synthetic.rs index ec59275c0188..fe645e194c75 100644 --- a/nautilus_core/model/src/instruments/synthetic.rs +++ b/nautilus_core/model/src/instruments/synthetic.rs @@ -18,7 +18,6 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::anyhow; use evalexpr::{ContextWithMutableVariables, HashMapContext, Node, Value}; use nautilus_core::time::UnixNanos; @@ -85,7 +84,7 @@ impl SyntheticInstrument { evalexpr::build_operator_tree(formula).is_ok() } - pub fn change_formula(&mut self, formula: String) -> Result<(), anyhow::Error> { + pub fn change_formula(&mut self, formula: String) -> anyhow::Result<()> { let operator_tree = evalexpr::build_operator_tree(&formula)?; self.formula = formula; self.operator_tree = operator_tree; @@ -115,7 +114,7 @@ impl SyntheticInstrument { /// provided as an array of `f64` values. pub fn calculate(&mut self, inputs: &[f64]) -> anyhow::Result { if inputs.len() != self.variables.len() { - return Err(anyhow!("Invalid number of input values")); + return Err(anyhow::anyhow!("Invalid number of input values")); } for (variable, input) in self.variables.iter().zip(inputs) { @@ -127,7 +126,7 @@ impl SyntheticInstrument { match result { Value::Float(price) => Price::new(price, self.price_precision), - _ => Err(anyhow!( + _ => Err(anyhow::anyhow!( "Failed to evaluate formula to a floating point number" )), } diff --git a/nautilus_core/model/src/orders/default.rs b/nautilus_core/model/src/orders/default.rs index 9f564ae04f57..0dfe0f39b24a 100644 --- a/nautilus_core/model/src/orders/default.rs +++ b/nautilus_core/model/src/orders/default.rs @@ -60,6 +60,7 @@ impl Default for LimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -95,6 +96,7 @@ impl Default for LimitIfTouchedOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -122,7 +124,7 @@ impl Default for MarketOrder { None, None, ) - .unwrap() + .unwrap() // SAFETY: Valid default values are used } } @@ -156,6 +158,7 @@ impl Default for MarketIfTouchedOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -186,6 +189,7 @@ impl Default for MarketToLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -221,6 +225,7 @@ impl Default for StopLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -254,6 +259,7 @@ impl Default for StopMarketOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -292,6 +298,7 @@ impl Default for TrailingStopLimitOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } @@ -327,5 +334,6 @@ impl Default for TrailingStopMarketOrder { UUID4::default(), 0, ) + .unwrap() // SAFETY: Valid default values are used } } diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index fcbaebb9fdfd..b4d68afe1573 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -53,7 +53,6 @@ pub struct LimitOrder { } impl LimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -81,8 +80,8 @@ impl LimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -111,7 +110,7 @@ impl LimitOrder { is_post_only: post_only, display_qty, trigger_instrument_id, - } + }) } } @@ -380,5 +379,6 @@ impl From for LimitOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index a638dea112f1..d98ea11ba84f 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -56,7 +56,6 @@ pub struct LimitIfTouchedOrder { } impl LimitIfTouchedOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -86,8 +85,8 @@ impl LimitIfTouchedOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -120,7 +119,7 @@ impl LimitIfTouchedOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -395,5 +394,6 @@ impl From for LimitIfTouchedOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 66fb95457999..1f382302b751 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -18,7 +18,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::{bail, Result}; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use ustr::Ustr; @@ -73,10 +72,10 @@ impl MarketOrder { exec_algorithm_params: Option>, exec_spawn_id: Option, tags: Option, - ) -> Result { + ) -> anyhow::Result { check_quantity_positive(quantity)?; if time_in_force == TimeInForce::Gtd { - bail!("{}", "GTD not supported for Market orders"); + anyhow::bail!("{}", "GTD not supported for Market orders"); } Ok(Self { @@ -356,7 +355,7 @@ impl From for MarketOrder { event.exec_spawn_id, event.tags, ) - .unwrap() + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index 537a0614da22..8ed0cdf49dda 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -54,7 +54,6 @@ pub struct MarketIfTouchedOrder { } impl MarketIfTouchedOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -82,8 +81,8 @@ impl MarketIfTouchedOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -114,7 +113,7 @@ impl MarketIfTouchedOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -382,6 +381,6 @@ impl From for MarketIfTouchedOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index a704cd8d6854..b32ac9ef8bd3 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -52,7 +52,6 @@ pub struct MarketToLimitOrder { } impl MarketToLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -77,8 +76,8 @@ impl MarketToLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -106,7 +105,7 @@ impl MarketToLimitOrder { expire_time, is_post_only: post_only, display_qty, - } + }) } } @@ -370,5 +369,6 @@ impl From for MarketToLimitOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 33dddb94a8ad..4624ddac0e8f 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -56,7 +56,6 @@ pub struct StopLimitOrder { } impl StopLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -86,8 +85,8 @@ impl StopLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -120,7 +119,7 @@ impl StopLimitOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -395,5 +394,6 @@ impl From for StopLimitOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 47931c5cccc6..efdaaa0028e8 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -55,7 +55,6 @@ pub struct StopMarketOrder { } impl StopMarketOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -83,8 +82,8 @@ impl StopMarketOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -115,7 +114,7 @@ impl StopMarketOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -384,5 +383,6 @@ impl From for StopMarketOrder { event.event_id, event.ts_event, ) + .unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/stubs.rs b/nautilus_core/model/src/orders/stubs.rs index 0ce159bedc72..7d623bacf790 100644 --- a/nautilus_core/model/src/orders/stubs.rs +++ b/nautilus_core/model/src/orders/stubs.rs @@ -178,6 +178,7 @@ impl TestOrderStubs { UUID4::new(), 12_321_312_321_312, ) + .unwrap() } #[must_use] @@ -222,5 +223,6 @@ impl TestOrderStubs { UUID4::new(), 12_321_312_321_312, ) + .unwrap() } } diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index b157b2dc3835..d838a190a7ca 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -59,7 +59,6 @@ pub struct TrailingStopLimitOrder { } impl TrailingStopLimitOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -92,8 +91,8 @@ impl TrailingStopLimitOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -129,7 +128,7 @@ impl TrailingStopLimitOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -406,6 +405,6 @@ impl From for TrailingStopLimitOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index 0fd025873aa1..de3a00c005cc 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -57,7 +57,6 @@ pub struct TrailingStopMarketOrder { } impl TrailingStopMarketOrder { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( trader_id: TraderId, @@ -87,8 +86,8 @@ impl TrailingStopMarketOrder { tags: Option, init_id: UUID4, ts_init: UnixNanos, - ) -> Self { - Self { + ) -> anyhow::Result { + Ok(Self { core: OrderCore::new( trader_id, strategy_id, @@ -121,7 +120,7 @@ impl TrailingStopMarketOrder { trigger_instrument_id, is_triggered: false, ts_triggered: None, - } + }) } } @@ -391,6 +390,6 @@ impl From for TrailingStopMarketOrder { event.tags, event.event_id, event.ts_event, - ) + ).unwrap() // SAFETY: From can panic } } diff --git a/nautilus_core/model/src/position.rs b/nautilus_core/model/src/position.rs index 722b89c71356..e941d724fb52 100644 --- a/nautilus_core/model/src/position.rs +++ b/nautilus_core/model/src/position.rs @@ -19,7 +19,6 @@ use std::{ hash::{Hash, Hasher}, }; -use anyhow::Result; use nautilus_core::time::UnixNanos; use serde::{Deserialize, Serialize}; @@ -82,7 +81,7 @@ pub struct Position { } impl Position { - pub fn new(instrument: T, fill: OrderFilled) -> Result { + pub fn new(instrument: T, fill: OrderFilled) -> anyhow::Result { assert_eq!(instrument.id(), fill.instrument_id); assert!(fill.position_id.is_some()); assert_ne!(fill.order_side, OrderSide::NoOrderSide); diff --git a/nautilus_core/model/src/stubs.rs b/nautilus_core/model/src/stubs.rs index b71abb032446..0d146381343b 100644 --- a/nautilus_core/model/src/stubs.rs +++ b/nautilus_core/model/src/stubs.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::Result; use rstest::fixture; use rust_decimal::prelude::ToPrimitive; @@ -37,7 +36,7 @@ pub fn calculate_commission( last_qty: Quantity, last_px: Price, use_quote_for_inverse: Option, -) -> Result { +) -> anyhow::Result { let liquidity_side = LiquiditySide::Taker; assert_ne!( liquidity_side, diff --git a/nautilus_core/model/src/types/balance.rs b/nautilus_core/model/src/types/balance.rs index f4edefe0dd27..b661a7c9737d 100644 --- a/nautilus_core/model/src/types/balance.rs +++ b/nautilus_core/model/src/types/balance.rs @@ -15,7 +15,6 @@ use std::fmt::{Display, Formatter}; -use anyhow::Result; use serde::{Deserialize, Serialize}; use crate::{ @@ -36,7 +35,7 @@ pub struct AccountBalance { } impl AccountBalance { - pub fn new(total: Money, locked: Money, free: Money) -> Result { + pub fn new(total: Money, locked: Money, free: Money) -> anyhow::Result { assert!(total == locked + free, "Total balance is not equal to the sum of locked and free balances: {total} != {locked} + {free}" ); @@ -78,7 +77,11 @@ pub struct MarginBalance { } impl MarginBalance { - pub fn new(initial: Money, maintenance: Money, instrument_id: InstrumentId) -> Result { + pub fn new( + initial: Money, + maintenance: Money, + instrument_id: InstrumentId, + ) -> anyhow::Result { Ok(Self { initial, maintenance, diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index c9a1cf9a0670..53c38273f4f0 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -18,7 +18,6 @@ use std::{ str::FromStr, }; -use anyhow::{anyhow, Result}; use nautilus_core::correctness::check_valid_string; use serde::{Deserialize, Serialize, Serializer}; use ustr::Ustr; @@ -47,7 +46,7 @@ impl Currency { iso4217: u16, name: &str, currency_type: CurrencyType, - ) -> Result { + ) -> anyhow::Result { check_valid_string(code, "`Currency` code")?; check_valid_string(name, "`Currency` name")?; check_fixed_precision(precision)?; @@ -61,8 +60,10 @@ impl Currency { }) } - pub fn register(currency: Self, overwrite: bool) -> Result<()> { - let mut map = CURRENCY_MAP.lock().map_err(|e| anyhow!(e.to_string()))?; + pub fn register(currency: Self, overwrite: bool) -> anyhow::Result<()> { + let mut map = CURRENCY_MAP + .lock() + .map_err(|e| anyhow::anyhow!(e.to_string()))?; if !overwrite && map.contains_key(currency.code.as_str()) { // If overwrite is false and the currency already exists, simply return @@ -74,17 +75,17 @@ impl Currency { Ok(()) } - pub fn is_fiat(code: &str) -> Result { + pub fn is_fiat(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::Fiat) } - pub fn is_crypto(code: &str) -> Result { + pub fn is_crypto(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::Crypto) } - pub fn is_commodity_backed(code: &str) -> Result { + pub fn is_commodity_backed(code: &str) -> anyhow::Result { let currency = Self::from_str(code)?; Ok(currency.currency_type == CurrencyType::CommodityBacked) } @@ -105,14 +106,14 @@ impl Hash for Currency { impl FromStr for Currency { type Err = anyhow::Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> anyhow::Result { let map_guard = CURRENCY_MAP .lock() - .map_err(|e| anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?; + .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `CURRENCY_MAP`: {e}"))?; map_guard .get(s) .copied() - .ok_or_else(|| anyhow!("Unknown currency: {s}")) + .ok_or_else(|| anyhow::anyhow!("Unknown currency: {s}")) } } diff --git a/nautilus_core/model/src/types/fixed.rs b/nautilus_core/model/src/types/fixed.rs index 21d4be2be6f3..ff7f5d2fbb74 100644 --- a/nautilus_core/model/src/types/fixed.rs +++ b/nautilus_core/model/src/types/fixed.rs @@ -13,14 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use anyhow::{bail, Result}; - pub const FIXED_PRECISION: u8 = 9; pub const FIXED_SCALAR: f64 = 1_000_000_000.0; // 10.0**FIXED_PRECISION -pub fn check_fixed_precision(precision: u8) -> Result<()> { +pub fn check_fixed_precision(precision: u8) -> anyhow::Result<()> { if precision > FIXED_PRECISION { - bail!("Condition failed: `precision` was greater than the maximum `FIXED_PRECISION` (9), was {precision}") + anyhow::bail!("Condition failed: `precision` was greater than the maximum `FIXED_PRECISION` (9), was {precision}") } Ok(()) } diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 50048eeff7c5..f6f7c51d4fa2 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -21,7 +21,6 @@ use std::{ str::FromStr, }; -use anyhow::Result; use nautilus_core::correctness::check_f64_in_range_inclusive; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; @@ -48,7 +47,7 @@ pub struct Money { } impl Money { - pub fn new(amount: f64, currency: Currency) -> Result { + pub fn new(amount: f64, currency: Currency) -> anyhow::Result { check_f64_in_range_inclusive(amount, MONEY_MIN, MONEY_MAX, "`Money` amount")?; Ok(Self { diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index 116ca2d5a381..d3a27f7633c1 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -21,7 +21,6 @@ use std::{ str::FromStr, }; -use anyhow::Result; use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; @@ -51,7 +50,7 @@ pub struct Price { } impl Price { - pub fn new(value: f64, precision: u8) -> Result { + pub fn new(value: f64, precision: u8) -> anyhow::Result { check_f64_in_range_inclusive(value, PRICE_MIN, PRICE_MAX, "`Price` value")?; check_fixed_precision(precision)?; @@ -61,7 +60,7 @@ impl Price { }) } - pub fn from_raw(raw: i64, precision: u8) -> Result { + pub fn from_raw(raw: i64, precision: u8) -> anyhow::Result { check_fixed_precision(precision)?; Ok(Self { raw, precision }) } diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 545dd740a0fd..c38b5bb2bf3e 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -21,7 +21,6 @@ use std::{ str::FromStr, }; -use anyhow::{bail, Result}; use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; @@ -45,7 +44,7 @@ pub struct Quantity { } impl Quantity { - pub fn new(value: f64, precision: u8) -> Result { + pub fn new(value: f64, precision: u8) -> anyhow::Result { check_f64_in_range_inclusive(value, QUANTITY_MIN, QUANTITY_MAX, "`Quantity` value")?; check_fixed_precision(precision)?; @@ -55,7 +54,7 @@ impl Quantity { }) } - pub fn from_raw(raw: u64, precision: u8) -> Result { + pub fn from_raw(raw: u64, precision: u8) -> anyhow::Result { check_fixed_precision(precision)?; Ok(Self { raw, precision }) } @@ -278,9 +277,9 @@ impl<'de> Deserialize<'de> for Quantity { } } -pub fn check_quantity_positive(value: Quantity) -> Result<()> { +pub fn check_quantity_positive(value: Quantity) -> anyhow::Result<()> { if !value.is_positive() { - bail!("Condition failed: invalid `Quantity`, should be positive and was {value}") + anyhow::bail!("Condition failed: invalid `Quantity`, should be positive and was {value}") } Ok(()) } diff --git a/nautilus_core/persistence/src/db/database.rs b/nautilus_core/persistence/src/db/database.rs index 3a917c072944..43630b1bd370 100644 --- a/nautilus_core/persistence/src/db/database.rs +++ b/nautilus_core/persistence/src/db/database.rs @@ -15,7 +15,6 @@ use std::{path::Path, str::FromStr}; -use anyhow::Result; use sqlx::{ any::{install_default_drivers, AnyConnectOptions}, sqlite::SqliteConnectOptions, @@ -108,7 +107,7 @@ impl Database { } } -pub async fn init_db_schema(db: &Database, schema_dir: &str) -> Result<()> { +pub async fn init_db_schema(db: &Database, schema_dir: &str) -> anyhow::Result<()> { // scan all the files in the current directory let mut sql_files = std::fs::read_dir(schema_dir)?.collect::, std::io::Error>>()?; From 84b3bb5ac48333cc87e8e0095d69f6f083975d19 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 11:53:32 +1100 Subject: [PATCH 20/71] Add TRADE_ID_LEN constant --- .../model/src/identifiers/trade_id.rs | 13 ++++++--- nautilus_trader/core/includes/model.h | 27 ++++++++----------- nautilus_trader/core/rust/model.pxd | 21 +++++++-------- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 7362503b8dfc..eaf5942d1633 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -22,6 +22,9 @@ use std::{ use nautilus_core::correctness::check_valid_string; use serde::{Deserialize, Deserializer, Serialize}; +/// The maximum length of ASCII characters for a `TradeId` string value. +const TRADE_ID_LEN: usize = 36; + /// Represents a valid trade match ID (assigned by a trading venue). /// /// Maximum length is 36 characters. @@ -37,7 +40,7 @@ use serde::{Deserialize, Deserializer, Serialize}; )] pub struct TradeId { /// The trade match ID C string value as a fixed-length byte array. - pub(crate) value: [u8; 37], + pub(crate) value: [u8; TRADE_ID_LEN + 1], } impl TradeId { @@ -53,10 +56,12 @@ impl TradeId { // TODO: Temporarily make this 65 to accommodate Betfair trade IDs // TODO: Extract this to single function let bytes = cstr.as_bytes_with_nul(); - if bytes.len() > 37 { - anyhow::bail!("Condition failed: value exceeds maximum trade ID length of 36"); + if bytes.len() > TRADE_ID_LEN + 1 { + anyhow::bail!( + "Condition failed: value exceeds maximum trade ID length of {TRADE_ID_LEN}" + ); } - let mut value = [0; 37]; + let mut value = [0; TRADE_ID_LEN + 1]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index bdd87dbbc51d..7278a629ea83 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -689,6 +689,17 @@ typedef struct OrderBookDeltas_t OrderBookDeltas_t; */ typedef struct SyntheticInstrument SyntheticInstrument; +/** + * Represents a valid trade match ID (assigned by a trading venue). + * + * Maximum length is 36 characters. + * Can correspond to the `TradeID <1003> field` of the FIX protocol. + * + * The unique ID assigned to the trade entity once it is received or matched by + * the exchange or central counterparty. + */ +typedef struct TradeId_t TradeId_t; + /** * Represents a valid ticker symbol ID for a tradable financial market instrument. */ @@ -889,22 +900,6 @@ typedef struct QuoteTick_t { uint64_t ts_init; } QuoteTick_t; -/** - * Represents a valid trade match ID (assigned by a trading venue). - * - * Maximum length is 36 characters. - * Can correspond to the `TradeID <1003> field` of the FIX protocol. - * - * The unique ID assigned to the trade entity once it is received or matched by - * the exchange or central counterparty. - */ -typedef struct TradeId_t { - /** - * The trade match ID C string value as a fixed-length byte array. - */ - uint8_t value[37]; -} TradeId_t; - /** * Represents a single trade tick in a financial market. */ diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 0d2de60ffd04..04e8919346dd 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -377,6 +377,16 @@ cdef extern from "../includes/model.h": cdef struct SyntheticInstrument: pass + # Represents a valid trade match ID (assigned by a trading venue). + # + # Maximum length is 36 characters. + # Can correspond to the `TradeID <1003> field` of the FIX protocol. + # + # The unique ID assigned to the trade entity once it is received or matched by + # the exchange or central counterparty. + cdef struct TradeId_t: + pass + # Represents a valid ticker symbol ID for a tradable financial market instrument. cdef struct Symbol_t: # The ticker symbol ID value. @@ -489,17 +499,6 @@ cdef extern from "../includes/model.h": # The UNIX timestamp (nanoseconds) when the data object was initialized. uint64_t ts_init; - # Represents a valid trade match ID (assigned by a trading venue). - # - # Maximum length is 36 characters. - # Can correspond to the `TradeID <1003> field` of the FIX protocol. - # - # The unique ID assigned to the trade entity once it is received or matched by - # the exchange or central counterparty. - cdef struct TradeId_t: - # The trade match ID C string value as a fixed-length byte array. - uint8_t value[37]; - # Represents a single trade tick in a financial market. cdef struct TradeTick_t: # The trade instrument ID. From c63701f3d61748e424c2306e2619556126c5da72 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 11:59:21 +1100 Subject: [PATCH 21/71] Add UUID4_LEN constant --- nautilus_core/core/src/uuid.rs | 11 +++++++---- nautilus_trader/core/includes/core.h | 17 ++++++----------- nautilus_trader/core/rust/core.pxd | 11 +++++------ 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 6c27aa312804..7d110eca937c 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -23,6 +23,9 @@ use std::{ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; +/// The maximum length of ASCII characters for a `UUID4` string value. +const UUID4_LEN: usize = 36; + /// Represents a pseudo-random UUID (universally unique identifier) /// version 4 based on a 128-bit label as specified in RFC 4122. #[repr(C)] @@ -33,7 +36,7 @@ use uuid::Uuid; )] pub struct UUID4 { /// The UUID v4 C string value as a fixed-length byte array. - pub(crate) value: [u8; 37], + pub(crate) value: [u8; UUID4_LEN + 1], } impl UUID4 { @@ -42,7 +45,7 @@ impl UUID4 { let uuid = Uuid::new_v4(); let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; 37]; + let mut value = [0; UUID4_LEN + 1]; value[..bytes.len()].copy_from_slice(bytes); Self { value } @@ -62,7 +65,7 @@ impl FromStr for UUID4 { let uuid = Uuid::parse_str(s).map_err(|_| "Invalid UUID string")?; let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; 37]; + let mut value = [0; UUID4_LEN + 1]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) @@ -123,7 +126,7 @@ mod tests { let uuid_string = uuid.to_string(); let uuid_parsed = Uuid::parse_str(&uuid_string).expect("Uuid::parse_str failed"); assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random); - assert_eq!(uuid_parsed.to_string().len(), 36); + assert_eq!(uuid_parsed.to_string().len(), UUID4_LEN); } #[rstest] diff --git a/nautilus_trader/core/includes/core.h b/nautilus_trader/core/includes/core.h index e91027c98faa..0b2e2c19769a 100644 --- a/nautilus_trader/core/includes/core.h +++ b/nautilus_trader/core/includes/core.h @@ -13,6 +13,12 @@ #define NANOSECONDS_IN_MICROSECOND 1000 +/** + * Represents a pseudo-random UUID (universally unique identifier) + * version 4 based on a 128-bit label as specified in RFC 4122. + */ +typedef struct UUID4_t UUID4_t; + /** * `CVec` is a C compatible struct that stores an opaque pointer to a block of * memory, it's length and the capacity of the vector it was allocated from. @@ -37,17 +43,6 @@ typedef struct CVec { uintptr_t cap; } CVec; -/** - * Represents a pseudo-random UUID (universally unique identifier) - * version 4 based on a 128-bit label as specified in RFC 4122. - */ -typedef struct UUID4_t { - /** - * The UUID v4 C string value as a fixed-length byte array. - */ - uint8_t value[37]; -} UUID4_t; - /** * Converts seconds to nanoseconds (ns). */ diff --git a/nautilus_trader/core/rust/core.pxd b/nautilus_trader/core/rust/core.pxd index d76a944ffb53..6fce2d74c60c 100644 --- a/nautilus_trader/core/rust/core.pxd +++ b/nautilus_trader/core/rust/core.pxd @@ -12,6 +12,11 @@ cdef extern from "../includes/core.h": const uint64_t NANOSECONDS_IN_MICROSECOND # = 1000 + # Represents a pseudo-random UUID (universally unique identifier) + # version 4 based on a 128-bit label as specified in RFC 4122. + cdef struct UUID4_t: + pass + # `CVec` is a C compatible struct that stores an opaque pointer to a block of # memory, it's length and the capacity of the vector it was allocated from. # @@ -27,12 +32,6 @@ cdef extern from "../includes/core.h": # Used when deallocating the memory uintptr_t cap; - # Represents a pseudo-random UUID (universally unique identifier) - # version 4 based on a 128-bit label as specified in RFC 4122. - cdef struct UUID4_t: - # The UUID v4 C string value as a fixed-length byte array. - uint8_t value[37]; - # Converts seconds to nanoseconds (ns). uint64_t secs_to_nanos(double secs); From 9ba8ed295112313f720c06a2a97d36ae5cbaf5ba Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 12:02:37 +1100 Subject: [PATCH 22/71] Update integrations status --- README.md | 12 ++++++------ docs/integrations/index.md | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cc300b40a8f8..66587f577ec2 100644 --- a/README.md +++ b/README.md @@ -141,12 +141,12 @@ into a unified interface. The following integrations are currently supported: | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :-------------------- | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------ | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports betting exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | -| [Binance](https://binance.com) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance US](https://binance.us) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | -| [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | +| [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | Refer to the [Integrations](https://docs.nautilustrader.io/integrations/index.html) documentation for further details. diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 6609b559f6af..1861de65b06f 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -26,12 +26,12 @@ running strategies which are able to access larger capital allocations. | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :-------------------- | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------ | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | | [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | | [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | -| [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | +| [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | ## Implementation goals From 585e7f85813a2009e921f825be0212185969b3f5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 12:03:52 +1100 Subject: [PATCH 23/71] Standardize comments --- nautilus_core/execution/src/matching_core.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/execution/src/matching_core.rs b/nautilus_core/execution/src/matching_core.rs index c502e5f1e387..630366b73ec9 100644 --- a/nautilus_core/execution/src/matching_core.rs +++ b/nautilus_core/execution/src/matching_core.rs @@ -342,7 +342,7 @@ mod tests { #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("100.00"), // Price below ask + Price::from("100.00"), // <-- Price below ask OrderSide::Buy, false )] @@ -413,7 +413,7 @@ mod tests { #[case( Some(Price::from("100.00")), Some(Price::from("101.00")), - Price::from("102.00"), // Trigger above ask + Price::from("102.00"), // <-- Trigger above ask OrderSide::Buy, false )] From fbab07a7b88ff15a9c919cc9e5597c4e551d5d62 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 12:28:47 +1100 Subject: [PATCH 24/71] Refine correctness check functions --- nautilus_core/common/src/clock.rs | 8 +- nautilus_core/common/src/timer.rs | 4 +- nautilus_core/core/src/correctness.rs | 90 +++++++++++-------- nautilus_core/model/src/data/quote.rs | 6 +- .../model/src/identifiers/account_id.rs | 4 +- .../model/src/identifiers/client_id.rs | 2 +- .../model/src/identifiers/client_order_id.rs | 2 +- .../model/src/identifiers/component_id.rs | 2 +- .../src/identifiers/exec_algorithm_id.rs | 2 +- .../model/src/identifiers/order_list_id.rs | 2 +- .../model/src/identifiers/position_id.rs | 2 +- .../model/src/identifiers/strategy_id.rs | 4 +- nautilus_core/model/src/identifiers/symbol.rs | 2 +- .../model/src/identifiers/trade_id.rs | 4 +- .../model/src/identifiers/trader_id.rs | 4 +- nautilus_core/model/src/identifiers/venue.rs | 4 +- .../model/src/identifiers/venue_order_id.rs | 2 +- .../model/src/instruments/crypto_future.rs | 6 +- .../model/src/instruments/crypto_perpetual.rs | 6 +- .../model/src/instruments/currency_pair.rs | 6 +- nautilus_core/model/src/instruments/equity.rs | 8 +- .../model/src/instruments/futures_contract.rs | 4 +- .../model/src/instruments/futures_spread.rs | 4 +- .../model/src/instruments/options_contract.rs | 4 +- .../model/src/instruments/options_spread.rs | 4 +- nautilus_core/model/src/types/currency.rs | 6 +- nautilus_core/model/src/types/money.rs | 4 +- nautilus_core/model/src/types/price.rs | 4 +- nautilus_core/model/src/types/quantity.rs | 4 +- 29 files changed, 111 insertions(+), 93 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index d6dfd8b5c6f5..c22c56e5c144 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -184,7 +184,7 @@ impl Clock for TestClock { alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, "name").unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -211,7 +211,7 @@ impl Clock for TestClock { stop_time_ns: Option, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, "name").unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -313,7 +313,7 @@ impl Clock for LiveClock { mut alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, "name").unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -345,7 +345,7 @@ impl Clock for LiveClock { stop_time_ns: Option, callback: Option, ) { - check_valid_string(name, "`Timer` name").unwrap(); + check_valid_string(name, "name").unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 598d23d1c877..b6e7d7757e78 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -140,7 +140,7 @@ impl TestTimer { start_time_ns: UnixNanos, stop_time_ns: Option, ) -> Self { - check_valid_string(name, "`TestTimer` name").unwrap(); + check_valid_string(name, "name").unwrap(); Self { name: Ustr::from(name), @@ -232,7 +232,7 @@ impl LiveTimer { stop_time_ns: Option, callback: EventHandler, ) -> Self { - check_valid_string(name, "`TestTimer` name").unwrap(); + check_valid_string(name, "name").unwrap(); Self { name: Ustr::from(name), diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 0af19e184a4b..db60ed670ba9 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -22,28 +22,28 @@ const FAILED: &str = "Condition failed:"; /// - If `s` is an empty string. /// - If `s` consists solely of whitespace characters. /// - If `s` contains one or more non-ASCII characters. -pub fn check_valid_string(s: &str, desc: &str) -> anyhow::Result<()> { +pub fn check_valid_string(s: &str, param: &str) -> anyhow::Result<()> { if s.is_empty() { - anyhow::bail!("{FAILED} invalid string for {desc}, was empty") + anyhow::bail!("{FAILED} invalid string for '{param}', was empty") } else if s.chars().all(char::is_whitespace) { - anyhow::bail!("{FAILED} invalid string for {desc}, was all whitespace",) + anyhow::bail!("{FAILED} invalid string for '{param}', was all whitespace",) } else if !s.is_ascii() { - anyhow::bail!("{FAILED} invalid string for {desc} contained a non-ASCII char, was '{s}'",) + anyhow::bail!("{FAILED} invalid string for '{param}' contained a non-ASCII char, was '{s}'",) } else { Ok(()) } } /// Validates that the string `s` contains the pattern `pat`. -pub fn check_string_contains(s: &str, pat: &str, desc: &str) -> anyhow::Result<()> { +pub fn check_string_contains(s: &str, pat: &str, param: &str) -> anyhow::Result<()> { if !s.contains(pat) { - anyhow::bail!("{FAILED} invalid string for {desc} did not contain '{pat}', was '{s}'") + anyhow::bail!("{FAILED} invalid string for '{param}' did not contain '{pat}', was '{s}'") } Ok(()) } /// Validates that `u8` values are equal. -pub fn check_u8_equal(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> { +pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> { if lhs != rhs { anyhow::bail!( "{FAILED} '{lhs_param}' u8 of {lhs} was not equal to '{rhs_param}' u8 of {rhs}" @@ -52,48 +52,64 @@ pub fn check_u8_equal(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> any Ok(()) } +/// Validates that the `u64` value is positive (> 0). +pub fn check_positive_u64(value: u64, param: &str) -> anyhow::Result<()> { + if value == 0 { + anyhow::bail!("{FAILED} invalid u64 for '{param}' not positive, was {value}") + } + Ok(()) +} + +/// Validates that the `i64` value is positive (> 0). +pub fn check_positive_i64(value: i64, param: &str) -> anyhow::Result<()> { + if value <= 0 { + anyhow::bail!("{FAILED} invalid i64 for '{param}' not positive, was {value}") + } + Ok(()) +} + +/// Validates that the `f64` value is non-negative. +pub fn check_non_negative_f64(value: f64, param: &str) -> anyhow::Result<()> { + if value.is_nan() || value.is_infinite() { + anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") + } + if value < 0.0 { + anyhow::bail!("{FAILED} invalid f64 for '{param}' negative, was {value}") + } + Ok(()) +} + /// Validates that the `u8` value is in the inclusive range [`l`, `r`]. -pub fn check_u8_in_range_inclusive(value: u8, l: u8, r: u8, desc: &str) -> anyhow::Result<()> { +pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> anyhow::Result<()> { if value < l || value > r { - anyhow::bail!("{FAILED} invalid u8 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u8 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `u64` value is in the inclusive range [`l`, `r`]. -pub fn check_u64_in_range_inclusive(value: u64, l: u64, r: u64, desc: &str) -> anyhow::Result<()> { +pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> anyhow::Result<()> { if value < l || value > r { - anyhow::bail!("{FAILED} invalid u64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid u64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `i64` value is in the inclusive range [`l`, `r`]. -pub fn check_i64_in_range_inclusive(value: i64, l: i64, r: i64, desc: &str) -> anyhow::Result<()> { +pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> anyhow::Result<()> { if value < l || value > r { - anyhow::bail!("{FAILED} invalid i64 for {desc} not in range [{l}, {r}], was {value}") + anyhow::bail!("{FAILED} invalid i64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } /// Validates that the `f64` value is in the inclusive range [`l`, `r`]. -pub fn check_f64_in_range_inclusive(value: f64, l: f64, r: f64, desc: &str) -> anyhow::Result<()> { +pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { - anyhow::bail!("{FAILED} invalid f64 for {desc}, was {value}") + anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") } if value < l || value > r { - anyhow::bail!("{FAILED} invalid f64 for {desc} not in range [{l}, {r}], was {value}") - } - Ok(()) -} - -/// Validates that the `f64` value is non-negative. -pub fn check_f64_non_negative(value: f64, desc: &str) -> anyhow::Result<()> { - if value.is_nan() || value.is_infinite() { - anyhow::bail!("{FAILED} invalid f64 for {desc}, was {value}") - } - if value < 0.0 { - anyhow::bail!("{FAILED} invalid f64 for {desc} negative, was {value}") + anyhow::bail!("{FAILED} invalid f64 for '{param}' not in range [{l}, {r}], was {value}") } Ok(()) } @@ -148,7 +164,7 @@ mod tests { #[case] r: u8, #[case] desc: &str, ) { - assert!(check_u8_in_range_inclusive(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok()); } #[rstest] @@ -160,7 +176,7 @@ mod tests { #[case] lhs_param: &str, #[case] rhs_param: &str, ) { - assert!(check_u8_equal(lhs, rhs, lhs_param, rhs_param).is_err()); + assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_err()); } #[rstest] @@ -171,7 +187,7 @@ mod tests { #[case] lhs_param: &str, #[case] rhs_param: &str, ) { - assert!(check_u8_equal(lhs, rhs, lhs_param, rhs_param).is_ok()); + assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok()); } #[rstest] @@ -183,7 +199,7 @@ mod tests { #[case] r: u8, #[case] desc: &str, ) { - assert!(check_u8_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u8(value, l, r, desc).is_err()); } #[rstest] @@ -196,7 +212,7 @@ mod tests { #[case] r: u64, #[case] desc: &str, ) { - assert!(check_u64_in_range_inclusive(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_u64(value, l, r, desc).is_ok()); } #[rstest] @@ -208,7 +224,7 @@ mod tests { #[case] r: u64, #[case] desc: &str, ) { - assert!(check_u64_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u64(value, l, r, desc).is_err()); } #[rstest] @@ -221,7 +237,7 @@ mod tests { #[case] r: i64, #[case] desc: &str, ) { - assert!(check_i64_in_range_inclusive(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_i64(value, l, r, desc).is_ok()); } #[rstest] @@ -233,14 +249,14 @@ mod tests { #[case] r: i64, #[case] desc: &str, ) { - assert!(check_i64_in_range_inclusive(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_i64(value, l, r, desc).is_err()); } #[rstest] #[case(0.0, "value")] #[case(1.0, "value")] fn test_f64_non_negative_when_valid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_f64_non_negative(value, desc).is_ok()); + assert!(check_non_negative_f64(value, desc).is_ok()); } #[rstest] @@ -249,6 +265,6 @@ mod tests { #[case(f64::NEG_INFINITY, "value")] #[case(-0.1, "value")] fn test_f64_non_negative_when_invalid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_f64_non_negative(value, desc).is_err()); + assert!(check_non_negative_f64(value, desc).is_err()); } } diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index 81a1b7664795..a0405875679e 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -21,7 +21,7 @@ use std::{ }; use indexmap::IndexMap; -use nautilus_core::{correctness::check_u8_equal, serialization::Serializable, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, serialization::Serializable, time::UnixNanos}; use serde::{Deserialize, Serialize}; use crate::{ @@ -66,13 +66,13 @@ impl QuoteTick { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( bid_price.precision, ask_price.precision, "bid_price.precision", "ask_price.precision", )?; - check_u8_equal( + check_equal_u8( bid_size.precision, ask_size.precision, "bid_size.precision", diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index 524c39a77fb3..9ac209bc768f 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -41,8 +41,8 @@ pub struct AccountId { impl AccountId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`accountid` value")?; - check_string_contains(s, "-", "`AccountId` value")?; + check_valid_string(s, "value")?; + check_string_contains(s, "-", "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index 5d34bb3d52f3..56daf8fcfcee 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -35,7 +35,7 @@ pub struct ClientId { impl ClientId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`ClientId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index fa7cb744664d..7a3235ab8393 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -35,7 +35,7 @@ pub struct ClientOrderId { impl ClientOrderId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`ClientOrderId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index c6710272c23b..654fb87486cb 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -35,7 +35,7 @@ pub struct ComponentId { impl ComponentId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`ComponentId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs index 3250ba77869a..207432d2ebc6 100644 --- a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs +++ b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs @@ -35,7 +35,7 @@ pub struct ExecAlgorithmId { impl ExecAlgorithmId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`ExecAlgorithmId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index 442bc613fde1..a8af5f5563e8 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -35,7 +35,7 @@ pub struct OrderListId { impl OrderListId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`OrderListId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 5347c3f02132..8888fdddffdd 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -35,7 +35,7 @@ pub struct PositionId { impl PositionId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`PositionId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index 1e2cd9a6cb3d..342a043086a6 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -41,9 +41,9 @@ pub struct StrategyId { impl StrategyId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`StrategyId` value")?; + check_valid_string(s, "value")?; if s != "EXTERNAL" { - check_string_contains(s, "-", "`StrategyId` value")?; + check_string_contains(s, "-", "value")?; } Ok(Self { diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index 45034110a167..989bc2bb240d 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -35,7 +35,7 @@ pub struct Symbol { impl Symbol { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`Symbol` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index eaf5942d1633..a99077e35a25 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -51,10 +51,8 @@ impl TradeId { } pub fn from_cstr(cstr: CString) -> anyhow::Result { - check_valid_string(cstr.to_str()?, "`TradeId` value")?; + check_valid_string(cstr.to_str()?, "value")?; - // TODO: Temporarily make this 65 to accommodate Betfair trade IDs - // TODO: Extract this to single function let bytes = cstr.as_bytes_with_nul(); if bytes.len() > TRADE_ID_LEN + 1 { anyhow::bail!( diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 285685928a49..32daeada6fa5 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -41,8 +41,8 @@ pub struct TraderId { impl TraderId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`TraderId` value")?; - check_string_contains(s, "-", "`TraderId` value")?; + check_valid_string(s, "value")?; + check_string_contains(s, "-", "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index ed4da552399d..31bc7ae0a309 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -39,7 +39,7 @@ pub struct Venue { impl Venue { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`Venue` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), @@ -56,7 +56,7 @@ impl Venue { pub fn from_code(code: &str) -> anyhow::Result { let map_guard = VENUE_MAP .lock() - .map_err(|e| anyhow::anyhow!("Failed to acquire lock on `VENUE_MAP`: {e}"))?; + .map_err(|e| anyhow::anyhow!("Error acquiring lock on `VENUE_MAP`: {e}"))?; map_guard .get(code) .copied() diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index 70badcd00c0e..62d1cd5d9595 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -35,7 +35,7 @@ pub struct VenueOrderId { impl VenueOrderId { pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "`VenueOrderId` value")?; + check_valid_string(s, "value")?; Ok(Self { value: Ustr::from(s), diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 7eda889c5114..32eb4de2effb 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -91,13 +91,13 @@ impl CryptoFuture { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", "price_increment.precision", )?; - check_u8_equal( + check_equal_u8( size_precision, size_increment.precision, "size_precision", diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index bb655fb0a7d3..07d46fe6ce1a 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -89,13 +89,13 @@ impl CryptoPerpetual { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", "price_increment.precision", )?; - check_u8_equal( + check_equal_u8( size_precision, size_increment.precision, "size_precision", diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 4c567b80b5e6..d804275698e7 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -85,13 +85,13 @@ impl CurrencyPair { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", "price_increment.precision", )?; - check_u8_equal( + check_equal_u8( size_precision, size_increment.precision, "size_precision", diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 9e8011255efa..16352b435c95 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -18,7 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -79,12 +82,13 @@ impl Equity { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", "price_increment.precision", )?; + check_positive_i64(price_increment.raw, "price_increment.raw")?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 8e1442ae8b06..278435ff9596 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -87,7 +87,7 @@ impl FuturesContract { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index f1e525b93657..4c4ecad83fc0 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -89,7 +89,7 @@ impl FuturesSpread { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index cf09a8d5611b..f784b002ee10 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -91,7 +91,7 @@ impl OptionsContract { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index cc4ee7035773..8663d5636e75 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -18,7 +18,7 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_u8_equal, time::UnixNanos}; +use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -89,7 +89,7 @@ impl OptionsSpread { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { - check_u8_equal( + check_equal_u8( price_precision, price_increment.precision, "price_precision", diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 53c38273f4f0..28d0576e3870 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -47,8 +47,8 @@ impl Currency { name: &str, currency_type: CurrencyType, ) -> anyhow::Result { - check_valid_string(code, "`Currency` code")?; - check_valid_string(name, "`Currency` name")?; + check_valid_string(code, "code")?; + check_valid_string(name, "name")?; check_fixed_precision(precision)?; Ok(Self { @@ -152,7 +152,7 @@ mod tests { use crate::{enums::CurrencyType, types::currency::Currency}; #[rstest] - #[should_panic(expected = "`Currency` code")] + #[should_panic(expected = "code")] fn test_invalid_currency_code() { let _ = Currency::new("", 2, 840, "United States dollar", CurrencyType::Fiat).unwrap(); } diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index f6f7c51d4fa2..56b54a8f030b 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -21,7 +21,7 @@ use std::{ str::FromStr, }; -use nautilus_core::correctness::check_f64_in_range_inclusive; +use nautilus_core::correctness::check_in_range_inclusive_f64; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -48,7 +48,7 @@ pub struct Money { impl Money { pub fn new(amount: f64, currency: Currency) -> anyhow::Result { - check_f64_in_range_inclusive(amount, MONEY_MIN, MONEY_MAX, "`Money` amount")?; + check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?; Ok(Self { raw: f64_to_fixed_i64(amount, currency.precision), diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index d3a27f7633c1..f6173658af76 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -21,7 +21,7 @@ use std::{ str::FromStr, }; -use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; +use nautilus_core::{correctness::check_in_range_inclusive_f64, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -51,7 +51,7 @@ pub struct Price { impl Price { pub fn new(value: f64, precision: u8) -> anyhow::Result { - check_f64_in_range_inclusive(value, PRICE_MIN, PRICE_MAX, "`Price` value")?; + check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?; check_fixed_precision(precision)?; Ok(Self { diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index c38b5bb2bf3e..0f45df8d5031 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -21,7 +21,7 @@ use std::{ str::FromStr, }; -use nautilus_core::{correctness::check_f64_in_range_inclusive, parsing::precision_from_str}; +use nautilus_core::{correctness::check_in_range_inclusive_f64, parsing::precision_from_str}; use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; use thousands::Separable; @@ -45,7 +45,7 @@ pub struct Quantity { impl Quantity { pub fn new(value: f64, precision: u8) -> anyhow::Result { - check_f64_in_range_inclusive(value, QUANTITY_MIN, QUANTITY_MAX, "`Quantity` value")?; + check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?; check_fixed_precision(precision)?; Ok(Self { From e82f6889b50acf5dd4c24914ed1c735187048447 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 13:09:39 +1100 Subject: [PATCH 25/71] Refine Rust correctness checking functions --- nautilus_core/common/src/clock.rs | 6 ++-- nautilus_core/common/src/timer.rs | 4 +-- nautilus_core/core/src/correctness.rs | 33 +++++++++++++------ nautilus_core/core/src/uuid.rs | 16 ++++----- .../model/src/identifiers/account_id.rs | 8 ++--- .../model/src/identifiers/client_id.rs | 6 ++-- .../model/src/identifiers/client_order_id.rs | 6 ++-- .../model/src/identifiers/component_id.rs | 6 ++-- .../src/identifiers/exec_algorithm_id.rs | 6 ++-- .../model/src/identifiers/order_list_id.rs | 6 ++-- .../model/src/identifiers/position_id.rs | 6 ++-- .../model/src/identifiers/strategy_id.rs | 10 +++--- nautilus_core/model/src/identifiers/symbol.rs | 6 ++-- .../model/src/identifiers/trade_id.rs | 30 ++++++++--------- .../model/src/identifiers/trader_id.rs | 8 ++--- nautilus_core/model/src/identifiers/venue.rs | 6 ++-- .../model/src/identifiers/venue_order_id.rs | 6 ++-- nautilus_trader/core/includes/core.h | 17 ++++++---- nautilus_trader/core/includes/model.h | 28 +++++++++------- nautilus_trader/core/rust/core.pxd | 11 ++++--- nautilus_trader/core/rust/model.pxd | 22 +++++++------ tests/unit_tests/model/test_identifiers.py | 4 +-- .../unit_tests/model/test_identifiers_pyo3.py | 4 +-- 23 files changed, 139 insertions(+), 116 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index c22c56e5c144..b60f9a03fdc5 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -184,7 +184,7 @@ impl Clock for TestClock { alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -313,7 +313,7 @@ impl Clock for LiveClock { mut alert_time_ns: UnixNanos, callback: Option, ) { - check_valid_string(name, "name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" @@ -345,7 +345,7 @@ impl Clock for LiveClock { stop_time_ns: Option, callback: Option, ) { - check_valid_string(name, "name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); assert!( callback.is_some() | self.default_callback.is_some(), "All Python callbacks were `None`" diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index b6e7d7757e78..ea9dc9207568 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -140,7 +140,7 @@ impl TestTimer { start_time_ns: UnixNanos, stop_time_ns: Option, ) -> Self { - check_valid_string(name, "name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); Self { name: Ustr::from(name), @@ -232,7 +232,7 @@ impl LiveTimer { stop_time_ns: Option, callback: EventHandler, ) -> Self { - check_valid_string(name, "name").unwrap(); + check_valid_string(name, stringify!(name)).unwrap(); Self { name: Ustr::from(name), diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index db60ed670ba9..90f065913d5a 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -15,7 +15,7 @@ const FAILED: &str = "Condition failed:"; -/// Validates the content of a string `s`. +/// Validates the string `s` contains only ASCII characters and has symantic meaning. /// /// # Panics /// @@ -34,7 +34,7 @@ pub fn check_valid_string(s: &str, param: &str) -> anyhow::Result<()> { } } -/// Validates that the string `s` contains the pattern `pat`. +/// Validates the string `s` contains the pattern `pat`. pub fn check_string_contains(s: &str, pat: &str, param: &str) -> anyhow::Result<()> { if !s.contains(pat) { anyhow::bail!("{FAILED} invalid string for '{param}' did not contain '{pat}', was '{s}'") @@ -42,7 +42,7 @@ pub fn check_string_contains(s: &str, pat: &str, param: &str) -> anyhow::Result< Ok(()) } -/// Validates that `u8` values are equal. +/// Validates the `u8` values are equal. pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> anyhow::Result<()> { if lhs != rhs { anyhow::bail!( @@ -52,7 +52,7 @@ pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> any Ok(()) } -/// Validates that the `u64` value is positive (> 0). +/// Validates the `u64` value is positive (> 0). pub fn check_positive_u64(value: u64, param: &str) -> anyhow::Result<()> { if value == 0 { anyhow::bail!("{FAILED} invalid u64 for '{param}' not positive, was {value}") @@ -60,7 +60,7 @@ pub fn check_positive_u64(value: u64, param: &str) -> anyhow::Result<()> { Ok(()) } -/// Validates that the `i64` value is positive (> 0). +/// Validates the `i64` value is positive (> 0). pub fn check_positive_i64(value: i64, param: &str) -> anyhow::Result<()> { if value <= 0 { anyhow::bail!("{FAILED} invalid i64 for '{param}' not positive, was {value}") @@ -68,7 +68,7 @@ pub fn check_positive_i64(value: i64, param: &str) -> anyhow::Result<()> { Ok(()) } -/// Validates that the `f64` value is non-negative. +/// Validates the `f64` value is non-negative (< 0). pub fn check_non_negative_f64(value: f64, param: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") @@ -79,7 +79,7 @@ pub fn check_non_negative_f64(value: f64, param: &str) -> anyhow::Result<()> { Ok(()) } -/// Validates that the `u8` value is in the inclusive range [`l`, `r`]. +/// Validates the `u8` value is in range [`l`, `r`] (inclusive). pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> anyhow::Result<()> { if value < l || value > r { anyhow::bail!("{FAILED} invalid u8 for '{param}' not in range [{l}, {r}], was {value}") @@ -87,7 +87,7 @@ pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> anyh Ok(()) } -/// Validates that the `u64` value is in the inclusive range [`l`, `r`]. +/// Validates the `u64` value is range [`l`, `r`] (inclusive). pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> anyhow::Result<()> { if value < l || value > r { anyhow::bail!("{FAILED} invalid u64 for '{param}' not in range [{l}, {r}], was {value}") @@ -95,7 +95,7 @@ pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> Ok(()) } -/// Validates that the `i64` value is in the inclusive range [`l`, `r`]. +/// Validates the `i64` value is in range [`l`, `r`] (inclusive). pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> anyhow::Result<()> { if value < l || value > r { anyhow::bail!("{FAILED} invalid i64 for '{param}' not in range [{l}, {r}], was {value}") @@ -103,7 +103,7 @@ pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> Ok(()) } -/// Validates that the `f64` value is in the inclusive range [`l`, `r`]. +/// Validates the `f64` value is in range [`l`, `r`] (inclusive). pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> anyhow::Result<()> { if value.is_nan() || value.is_infinite() { anyhow::bail!("{FAILED} invalid f64 for '{param}', was {value}") @@ -114,6 +114,19 @@ pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> Ok(()) } +/// Validates the `usize` value is in range [`l`, `r`] (inclusive). +pub fn check_in_range_inclusive_usize( + value: usize, + l: usize, + r: usize, + param: &str, +) -> anyhow::Result<()> { + if value < l || value > r { + anyhow::bail!("{FAILED} invalid usize for '{param}' not in range [{l}, {r}], was {value}") + } + Ok(()) +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 7d110eca937c..324e49918f02 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -23,8 +23,8 @@ use std::{ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; -/// The maximum length of ASCII characters for a `UUID4` string value. -const UUID4_LEN: usize = 36; +/// The maximum length of ASCII characters for a `UUID4` string value (includes null terminator). +const UUID4_LEN: usize = 37; /// Represents a pseudo-random UUID (universally unique identifier) /// version 4 based on a 128-bit label as specified in RFC 4122. @@ -35,8 +35,8 @@ const UUID4_LEN: usize = 36; pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.core") )] pub struct UUID4 { - /// The UUID v4 C string value as a fixed-length byte array. - pub(crate) value: [u8; UUID4_LEN + 1], + /// The UUID v4 value as a fixed-length C string byte array (includes null terminator). + pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array } impl UUID4 { @@ -45,7 +45,7 @@ impl UUID4 { let uuid = Uuid::new_v4(); let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; UUID4_LEN + 1]; + let mut value = [0; UUID4_LEN]; value[..bytes.len()].copy_from_slice(bytes); Self { value } @@ -53,7 +53,7 @@ impl UUID4 { #[must_use] pub fn to_cstr(&self) -> &CStr { - // SAFETY: Unwrap safe as we always store valid C strings + // SAFETY: We always store valid C strings CStr::from_bytes_with_nul(&self.value).unwrap() } } @@ -65,7 +65,7 @@ impl FromStr for UUID4 { let uuid = Uuid::parse_str(s).map_err(|_| "Invalid UUID string")?; let c_string = CString::new(uuid.to_string()).expect("`CString` conversion failed"); let bytes = c_string.as_bytes_with_nul(); - let mut value = [0; UUID4_LEN + 1]; + let mut value = [0; UUID4_LEN]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) @@ -126,7 +126,7 @@ mod tests { let uuid_string = uuid.to_string(); let uuid_parsed = Uuid::parse_str(&uuid_string).expect("Uuid::parse_str failed"); assert_eq!(uuid_parsed.get_version().unwrap(), uuid::Version::Random); - assert_eq!(uuid_parsed.to_string().len(), UUID4_LEN); + assert_eq!(uuid_parsed.to_string().len(), 36); } #[rstest] diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index 9ac209bc768f..b4bb1634d4b5 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -40,12 +40,12 @@ pub struct AccountId { } impl AccountId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; - check_string_contains(s, "-", "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + check_string_contains(value, "-", stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index 56daf8fcfcee..54d2a6807b30 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -34,11 +34,11 @@ pub struct ClientId { } impl ClientId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index 7a3235ab8393..3bbb7dfd79fe 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -34,11 +34,11 @@ pub struct ClientOrderId { } impl ClientOrderId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index 654fb87486cb..0b607f7fae1f 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -34,11 +34,11 @@ pub struct ComponentId { } impl ComponentId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs index 207432d2ebc6..7cb4d9f967d0 100644 --- a/nautilus_core/model/src/identifiers/exec_algorithm_id.rs +++ b/nautilus_core/model/src/identifiers/exec_algorithm_id.rs @@ -34,11 +34,11 @@ pub struct ExecAlgorithmId { } impl ExecAlgorithmId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index a8af5f5563e8..6165204f5197 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -34,11 +34,11 @@ pub struct OrderListId { } impl OrderListId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 8888fdddffdd..954ac04a2cf3 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -34,11 +34,11 @@ pub struct PositionId { } impl PositionId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index 342a043086a6..dff74ac50bc2 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -40,14 +40,14 @@ pub struct StrategyId { } impl StrategyId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; - if s != "EXTERNAL" { - check_string_contains(s, "-", "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + if value != "EXTERNAL" { + check_string_contains(value, "-", stringify!(value))?; } Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index 989bc2bb240d..62118daf5419 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -34,11 +34,11 @@ pub struct Symbol { } impl Symbol { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index a99077e35a25..797d3cf48169 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -19,19 +19,20 @@ use std::{ hash::Hash, }; -use nautilus_core::correctness::check_valid_string; +use nautilus_core::correctness::{check_in_range_inclusive_usize, check_valid_string}; use serde::{Deserialize, Deserializer, Serialize}; -/// The maximum length of ASCII characters for a `TradeId` string value. -const TRADE_ID_LEN: usize = 36; +/// The maximum length of ASCII characters for a `TradeId` string value (including null terminator). +const TRADE_ID_LEN: usize = 37; /// Represents a valid trade match ID (assigned by a trading venue). /// /// Maximum length is 36 characters. -/// Can correspond to the `TradeID <1003> field` of the FIX protocol. /// /// The unique ID assigned to the trade entity once it is received or matched by /// the exchange or central counterparty. +/// +/// Can correspond to the `TradeID <1003> field` of the FIX protocol. #[repr(C)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( @@ -39,27 +40,22 @@ const TRADE_ID_LEN: usize = 36; pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") )] pub struct TradeId { - /// The trade match ID C string value as a fixed-length byte array. - pub(crate) value: [u8; TRADE_ID_LEN + 1], + /// The trade match ID value as a fixed-length C string byte array (includes null terminator). + pub(crate) value: [u8; 37], // cbindgen issue using the constant in the array } impl TradeId { - pub fn new(s: &str) -> anyhow::Result { - let cstr = CString::new(s).expect("`CString` conversion failed"); - + pub fn new(value: &str) -> anyhow::Result { + let cstr = CString::new(value).expect("`CString` conversion failed"); Self::from_cstr(cstr) } pub fn from_cstr(cstr: CString) -> anyhow::Result { - check_valid_string(cstr.to_str()?, "value")?; - let bytes = cstr.as_bytes_with_nul(); - if bytes.len() > TRADE_ID_LEN + 1 { - anyhow::bail!( - "Condition failed: value exceeds maximum trade ID length of {TRADE_ID_LEN}" - ); - } - let mut value = [0; TRADE_ID_LEN + 1]; + check_valid_string(cstr.to_str()?, stringify!(cstr))?; + check_in_range_inclusive_usize(bytes.len(), 2, TRADE_ID_LEN, stringify!(cstr))?; + + let mut value = [0; TRADE_ID_LEN]; value[..bytes.len()].copy_from_slice(bytes); Ok(Self { value }) diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index 32daeada6fa5..d71bfb1af5e1 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -40,12 +40,12 @@ pub struct TraderId { } impl TraderId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; - check_string_contains(s, "-", "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; + check_string_contains(value, "-", stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index 31bc7ae0a309..544aa98bd816 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -38,11 +38,11 @@ pub struct Venue { } impl Venue { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index 62d1cd5d9595..e347fdb89b07 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -34,11 +34,11 @@ pub struct VenueOrderId { } impl VenueOrderId { - pub fn new(s: &str) -> anyhow::Result { - check_valid_string(s, "value")?; + pub fn new(value: &str) -> anyhow::Result { + check_valid_string(value, stringify!(value))?; Ok(Self { - value: Ustr::from(s), + value: Ustr::from(value), }) } } diff --git a/nautilus_trader/core/includes/core.h b/nautilus_trader/core/includes/core.h index 0b2e2c19769a..0ba5678d6758 100644 --- a/nautilus_trader/core/includes/core.h +++ b/nautilus_trader/core/includes/core.h @@ -13,12 +13,6 @@ #define NANOSECONDS_IN_MICROSECOND 1000 -/** - * Represents a pseudo-random UUID (universally unique identifier) - * version 4 based on a 128-bit label as specified in RFC 4122. - */ -typedef struct UUID4_t UUID4_t; - /** * `CVec` is a C compatible struct that stores an opaque pointer to a block of * memory, it's length and the capacity of the vector it was allocated from. @@ -43,6 +37,17 @@ typedef struct CVec { uintptr_t cap; } CVec; +/** + * Represents a pseudo-random UUID (universally unique identifier) + * version 4 based on a 128-bit label as specified in RFC 4122. + */ +typedef struct UUID4_t { + /** + * The UUID v4 value as a fixed-length C string byte array (includes null terminator). + */ + uint8_t value[37]; +} UUID4_t; + /** * Converts seconds to nanoseconds (ns). */ diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 7278a629ea83..37fb2d608f0a 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -689,17 +689,6 @@ typedef struct OrderBookDeltas_t OrderBookDeltas_t; */ typedef struct SyntheticInstrument SyntheticInstrument; -/** - * Represents a valid trade match ID (assigned by a trading venue). - * - * Maximum length is 36 characters. - * Can correspond to the `TradeID <1003> field` of the FIX protocol. - * - * The unique ID assigned to the trade entity once it is received or matched by - * the exchange or central counterparty. - */ -typedef struct TradeId_t TradeId_t; - /** * Represents a valid ticker symbol ID for a tradable financial market instrument. */ @@ -900,6 +889,23 @@ typedef struct QuoteTick_t { uint64_t ts_init; } QuoteTick_t; +/** + * Represents a valid trade match ID (assigned by a trading venue). + * + * Maximum length is 36 characters. + * + * The unique ID assigned to the trade entity once it is received or matched by + * the exchange or central counterparty. + * + * Can correspond to the `TradeID <1003> field` of the FIX protocol. + */ +typedef struct TradeId_t { + /** + * The trade match ID value as a fixed-length C string byte array (includes null terminator). + */ + uint8_t value[37]; +} TradeId_t; + /** * Represents a single trade tick in a financial market. */ diff --git a/nautilus_trader/core/rust/core.pxd b/nautilus_trader/core/rust/core.pxd index 6fce2d74c60c..58acfd03d7f8 100644 --- a/nautilus_trader/core/rust/core.pxd +++ b/nautilus_trader/core/rust/core.pxd @@ -12,11 +12,6 @@ cdef extern from "../includes/core.h": const uint64_t NANOSECONDS_IN_MICROSECOND # = 1000 - # Represents a pseudo-random UUID (universally unique identifier) - # version 4 based on a 128-bit label as specified in RFC 4122. - cdef struct UUID4_t: - pass - # `CVec` is a C compatible struct that stores an opaque pointer to a block of # memory, it's length and the capacity of the vector it was allocated from. # @@ -32,6 +27,12 @@ cdef extern from "../includes/core.h": # Used when deallocating the memory uintptr_t cap; + # Represents a pseudo-random UUID (universally unique identifier) + # version 4 based on a 128-bit label as specified in RFC 4122. + cdef struct UUID4_t: + # The UUID v4 value as a fixed-length C string byte array (includes null terminator). + uint8_t value[37]; + # Converts seconds to nanoseconds (ns). uint64_t secs_to_nanos(double secs); diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 04e8919346dd..b33e267e4729 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -377,16 +377,6 @@ cdef extern from "../includes/model.h": cdef struct SyntheticInstrument: pass - # Represents a valid trade match ID (assigned by a trading venue). - # - # Maximum length is 36 characters. - # Can correspond to the `TradeID <1003> field` of the FIX protocol. - # - # The unique ID assigned to the trade entity once it is received or matched by - # the exchange or central counterparty. - cdef struct TradeId_t: - pass - # Represents a valid ticker symbol ID for a tradable financial market instrument. cdef struct Symbol_t: # The ticker symbol ID value. @@ -499,6 +489,18 @@ cdef extern from "../includes/model.h": # The UNIX timestamp (nanoseconds) when the data object was initialized. uint64_t ts_init; + # Represents a valid trade match ID (assigned by a trading venue). + # + # Maximum length is 36 characters. + # + # The unique ID assigned to the trade entity once it is received or matched by + # the exchange or central counterparty. + # + # Can correspond to the `TradeID <1003> field` of the FIX protocol. + cdef struct TradeId_t: + # The trade match ID value as a fixed-length C string byte array (includes null terminator). + uint8_t value[37]; + # Represents a single trade tick in a financial market. cdef struct TradeTick_t: # The trade instrument ID. diff --git a/tests/unit_tests/model/test_identifiers.py b/tests/unit_tests/model/test_identifiers.py index 43bc9d37541f..e32e02323cdb 100644 --- a/tests/unit_tests/model/test_identifiers.py +++ b/tests/unit_tests/model/test_identifiers.py @@ -208,11 +208,11 @@ def test_instrument_id_from_str() -> None: ], [ ".USDT", - "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for `Symbol` value, was empty", + "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for 'value', was empty", ], [ "BTC.", - "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for `Venue` value, was empty", + "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for 'value', was empty", ], ], ) diff --git a/tests/unit_tests/model/test_identifiers_pyo3.py b/tests/unit_tests/model/test_identifiers_pyo3.py index f6b42f7936d1..dfdf020c92e7 100644 --- a/tests/unit_tests/model/test_identifiers_pyo3.py +++ b/tests/unit_tests/model/test_identifiers_pyo3.py @@ -190,11 +190,11 @@ def test_instrument_id_from_str() -> None: ], [ ".USDT", - "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for `Symbol` value, was empty", + "Error parsing `InstrumentId` from '.USDT': Condition failed: invalid string for 'value', was empty", ], [ "BTC.", - "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for `Venue` value, was empty", + "Error parsing `InstrumentId` from 'BTC.': Condition failed: invalid string for 'value', was empty", ], ], ) From 8bd683d53a089fcce9ce2adad9462a176c7a9cc6 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Sun, 17 Mar 2024 05:12:27 +0300 Subject: [PATCH 26/71] Refactor InteractiveBrokersEWrapper (#1548) --- .../interactive_brokers/client/account.py | 17 +- .../interactive_brokers/client/client.py | 23 +- .../interactive_brokers/client/connection.py | 5 +- .../interactive_brokers/client/contract.py | 21 +- .../interactive_brokers/client/error.py | 25 +- .../interactive_brokers/client/market_data.py | 36 +- .../interactive_brokers/client/order.py | 24 +- .../interactive_brokers/client/wrapper.py | 1239 +++++++++++++++++ .../client/test_client_error.py | 30 +- .../client/test_client_market_data.py | 54 +- .../client/test_client_order.py | 44 +- .../adapters/interactive_brokers/conftest.py | 4 +- 12 files changed, 1363 insertions(+), 159 deletions(-) create mode 100644 nautilus_trader/adapters/interactive_brokers/client/wrapper.py diff --git a/nautilus_trader/adapters/interactive_brokers/client/account.py b/nautilus_trader/adapters/interactive_brokers/client/account.py index f5f1bd887feb..4a749a65d69b 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/account.py +++ b/nautilus_trader/adapters/interactive_brokers/client/account.py @@ -17,7 +17,6 @@ from decimal import Decimal from ibapi.account_summary_tags import AccountSummaryTags -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin from nautilus_trader.adapters.interactive_brokers.client.common import IBPosition @@ -134,10 +133,9 @@ async def get_positions(self, account_id: str) -> list[Position] | None: positions.append(position) return positions - # -- EWrapper overrides ----------------------------------------------------------------------- - - def accountSummary( + def process_account_summary( self, + *, req_id: int, account_id: str, tag: str, @@ -147,26 +145,25 @@ def accountSummary( """ Receive account information. """ - self.logAnswer(current_fn_name(), vars()) name = f"accountSummary-{account_id}" if handler := self._event_subscriptions.get(name, None): handler(tag, value, currency) - def managedAccounts(self, accounts_list: str) -> None: + def process_managed_accounts(self, *, accounts_list: str) -> None: """ Receive a comma-separated string with the managed account ids. Occurs automatically on initial API client connection. """ - self.logAnswer(current_fn_name(), vars()) self._account_ids = {a for a in accounts_list.split(",") if a} if self._next_valid_order_id >= 0 and not self._is_ib_ready.is_set(): self._log.info("`is_ib_ready` set by managedAccounts", LogColor.BLUE) self._is_ib_ready.set() - def position( + def process_position( self, + *, account_id: str, contract: IBContract, position: Decimal, @@ -175,14 +172,12 @@ def position( """ Provide the portfolio's open positions. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenPositions"): request.result.append(IBPosition(account_id, contract, position, avg_cost)) - def positionEnd(self) -> None: + def process_position_end(self) -> None: """ Indicate that all the positions have been transmitted. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenPositions"): self._end_request(request.req_id) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index a29358bd0460..ff009fa1dbbe 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -29,7 +29,6 @@ from ibapi.errors import BAD_LENGTH from ibapi.execution import Execution from ibapi.utils import current_fn_name -from ibapi.wrapper import EWrapper # fmt: off from nautilus_trader.adapters.interactive_brokers.client.account import InteractiveBrokersClientAccountMixin @@ -42,6 +41,7 @@ from nautilus_trader.adapters.interactive_brokers.client.error import InteractiveBrokersClientErrorMixin from nautilus_trader.adapters.interactive_brokers.client.market_data import InteractiveBrokersClientMarketDataMixin from nautilus_trader.adapters.interactive_brokers.client.order import InteractiveBrokersClientOrderMixin +from nautilus_trader.adapters.interactive_brokers.client.wrapper import InteractiveBrokersEWrapper from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.parsing.instruments import instrument_id_to_ib_contract @@ -65,7 +65,6 @@ class InteractiveBrokersClient( InteractiveBrokersClientOrderMixin, InteractiveBrokersClientContractMixin, InteractiveBrokersClientErrorMixin, - EWrapper, ): """ A client component that interfaces with the Interactive Brokers TWS or Gateway. @@ -103,7 +102,12 @@ def __init__( self._client_id = client_id # TWS API - self._eclient: EClient = EClient(wrapper=self) + self._eclient: EClient = EClient( + wrapper=InteractiveBrokersEWrapper( + nautilus_logger=self._log, + client=self, + ), + ) # Tasks self._watch_dog_task: asyncio.Task | None = None @@ -561,16 +565,3 @@ def logRequest(self, fnName, fnParams): else: prms = fnParams self._log.debug(f"TWS API prepared request: function={fnName} data={prms}") - - # -- EWrapper overrides ----------------------------------------------------------------------- - - def logAnswer(self, fnName, fnParams): - """ - Override the logging for EWrapper.logAnswer. - """ - if "self" in fnParams: - prms = dict(fnParams) - del prms["self"] - else: - prms = fnParams - self._log.debug(f"Msg handled: function={fnName} data={prms}") diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index ed485ae67c39..bac007b4065c 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -24,7 +24,6 @@ from ibapi.errors import CONNECT_FAIL from ibapi.server_versions import MAX_CLIENT_VER from ibapi.server_versions import MIN_CLIENT_VER -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin @@ -264,8 +263,7 @@ def _handle_connection_error(self, e): self._eclient.disconnect() self._log.error(f"Connection failed: {e}") - # -- EWrapper overrides ----------------------------------------------------------------------- - def connectionClosed(self) -> None: + def process_connection_closed(self) -> None: """ Indicate the API connection has closed. @@ -273,7 +271,6 @@ def connectionClosed(self) -> None: automatically but must be triggered by API client code. """ - self.logAnswer(current_fn_name(), vars()) for future in self._requests.get_futures(): if not future.done(): future.set_exception(ConnectionError("Socket disconnect")) diff --git a/nautilus_trader/adapters/interactive_brokers/client/contract.py b/nautilus_trader/adapters/interactive_brokers/client/contract.py index 9c284a9f9113..acfd98ec842e 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/contract.py +++ b/nautilus_trader/adapters/interactive_brokers/client/contract.py @@ -19,7 +19,6 @@ from ibapi.common import SetOfFloat from ibapi.common import SetOfString from ibapi.contract import ContractDetails -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin from nautilus_trader.adapters.interactive_brokers.common import IBContract @@ -140,9 +139,9 @@ async def get_option_chains(self, underlying: IBContract) -> Any | None: self._log.info(f"Request already exist for {request}") return None - # -- EWrapper overrides ----------------------------------------------------------------------- - def contractDetails( + def process_contract_details( self, + *, req_id: int, contract_details: ContractDetails, ) -> None: @@ -151,21 +150,20 @@ def contractDetails( contracts matching the requested via EClientSocket::reqContractDetails. For example, one can obtain the whole option chain with it. """ - self.logAnswer(current_fn_name(), vars()) if not (request := self._requests.get(req_id=req_id)): return request.result.append(contract_details) - def contractDetailsEnd(self, req_id: int) -> None: + def process_contract_details_end(self, *, req_id: int) -> None: """ After all contracts matching the request were returned, this method will mark the end of their reception. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) - def securityDefinitionOptionParameter( + def process_security_definition_option_parameter( self, + *, req_id: int, exchange: str, underlying_con_id: int, @@ -180,27 +178,24 @@ def securityDefinitionOptionParameter( securityDefinitionOptionParameter if multiple exchanges are specified in reqSecDefOptParams. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(req_id=req_id): request.result.append((exchange, expirations)) - def securityDefinitionOptionParameterEnd(self, req_id: int) -> None: + def process_security_definition_option_parameter_end(self, *, req_id: int) -> None: """ Call when all callbacks to securityDefinitionOptionParameter are complete. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) - def symbolSamples( + def process_symbol_samples( self, + *, req_id: int, contract_descriptions: list, ) -> None: """ Return an array of sample contract descriptions. """ - self.logAnswer(current_fn_name(), vars()) - if request := self._requests.get(req_id=req_id): for contract_description in contract_descriptions: request.result.append(IBContract(**contract_description.contract.__dict__)) diff --git a/nautilus_trader/adapters/interactive_brokers/client/error.py b/nautilus_trader/adapters/interactive_brokers/client/error.py index 94fa54d20d26..09511226bf2b 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/error.py +++ b/nautilus_trader/adapters/interactive_brokers/client/error.py @@ -66,7 +66,14 @@ def _log_message( else: self._log.error(msg) - def _process_error(self, req_id: int, error_code: int, error_string: str) -> None: + def process_error( + self, + *, + req_id: int, + error_code: int, + error_string: str, + advanced_order_reject_json: str = "", + ) -> None: """ Process an error based on its code, request ID, and message. Depending on the error code, this method delegates to specific error handlers or performs general @@ -80,6 +87,8 @@ def _process_error(self, req_id: int, error_code: int, error_string: str) -> Non The error code. error_string : str The error message string. + advanced_order_reject_json : str + The JSON string for advanced order rejection. """ is_warning = error_code in self.WARNING_CODES or 2100 <= error_code < 2200 @@ -197,17 +206,3 @@ def _handle_order_error(self, req_id: int, error_code: int, error_string: str) - f"Unhandled order warning or error code: {error_code} (req_id {req_id}) - " f"{error_string}", ) - - # -- EWrapper overrides ----------------------------------------------------------------------- - - def error( - self, - req_id: int, - error_code: int, - error_string: str, - advanced_order_reject_json: str = "", - ) -> None: - """ - Errors sent by TWS API are received here. - """ - self._process_error(req_id, error_code, error_string) diff --git a/nautilus_trader/adapters/interactive_brokers/client/market_data.py b/nautilus_trader/adapters/interactive_brokers/client/market_data.py index a3ecdb4cbd78..f12b71e8a9b7 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/market_data.py +++ b/nautilus_trader/adapters/interactive_brokers/client/market_data.py @@ -25,7 +25,6 @@ from ibapi.common import MarketDataTypeEnum from ibapi.common import TickAttribBidAsk from ibapi.common import TickAttribLast -from ibapi.utils import current_fn_name # fmt: off from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin @@ -646,21 +645,20 @@ def _handle_data(self, data: Data) -> None: """ self._msgbus.send(endpoint="DataEngine.process", msg=data) - # -- EWrapper overrides ----------------------------------------------------------------------- - def marketDataType(self, req_id: int, market_data_type: int) -> None: + def process_market_data_type(self, *, req_id: int, market_data_type: int) -> None: """ Return the market data type (real-time, frozen, delayed, delayed-frozen) of ticker sent by EClientSocket::reqMktData when TWS switches from real-time to frozen and back and from delayed to delayed-frozen and back. """ - self.logAnswer(current_fn_name(), vars()) if market_data_type == MarketDataTypeEnum.REALTIME: self._log.debug(f"Market DataType is {MarketDataTypeEnum.to_str(market_data_type)}") else: self._log.warning(f"Market DataType is {MarketDataTypeEnum.to_str(market_data_type)}") - def tickByTickBidAsk( + def process_tick_by_tick_bid_ask( self, + *, req_id: int, time: int, bid_price: float, @@ -672,7 +670,6 @@ def tickByTickBidAsk( """ Return "BidAsk" tick-by-tick real-time tick data. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return @@ -692,8 +689,9 @@ def tickByTickBidAsk( self._handle_data(quote_tick) - def tickByTickAllLast( + def process_tick_by_tick_all_last( self, + *, req_id: int, tick_type: str, time: int, @@ -706,7 +704,6 @@ def tickByTickAllLast( """ Return "Last" or "AllLast" (trades) tick-by-tick real-time tick. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return @@ -730,8 +727,9 @@ def tickByTickAllLast( self._handle_data(trade_tick) - def realtimeBar( + def process_realtime_bar( self, + *, req_id: int, time: int, open_: float, @@ -745,7 +743,6 @@ def realtimeBar( """ Update real-time 5 second bars. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return bar_type = BarType.from_str(subscription.name) @@ -765,11 +762,10 @@ def realtimeBar( self._handle_data(bar) - def historicalData(self, req_id: int, bar: BarData) -> None: + def process_historical_data(self, *, req_id: int, bar: BarData) -> None: """ Return the requested historical data bars. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(req_id=req_id): bar_type = BarType.from_str(request.name) bar = self._ib_bar_to_nautilus_bar( @@ -792,17 +788,16 @@ def historicalData(self, req_id: int, bar: BarData) -> None: self._log.debug(f"Received {bar=} on {req_id=}") return - def historicalDataEnd(self, req_id: int, start: str, end: str) -> None: + def process_historical_data_end(self, *, req_id: int, start: str, end: str) -> None: """ Mark the end of receiving historical bars. """ - self.logAnswer(current_fn_name(), vars()) self._end_request(req_id) if req_id == 1 and not self._is_ib_ready.is_set(): # probe successful self._log.info(f"`is_ib_ready` set by historicalDataEnd {req_id=}", LogColor.BLUE) self._is_ib_ready.set() - def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: + def process_historical_data_update(self, *, req_id: int, bar: BarData) -> None: """ Receive bars in real-time if keepUpToDate is set as True in reqHistoricalData. @@ -812,7 +807,6 @@ def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: time data. """ - self.logAnswer(current_fn_name(), vars()) if not (subscription := self._subscriptions.get(req_id=req_id)): return if not isinstance(subscription.handle, functools.partial): @@ -827,8 +821,9 @@ def historicalDataUpdate(self, req_id: int, bar: BarData) -> None: else: self._handle_data(bar) - def historicalTicksBidAsk( + def process_historical_ticks_bid_ask( self, + *, req_id: int, ticks: list, done: bool, @@ -836,7 +831,6 @@ def historicalTicksBidAsk( """ Return the requested historic bid/ask ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return if request := self._requests.get(req_id=req_id): @@ -858,20 +852,18 @@ def historicalTicksBidAsk( self._end_request(req_id) - def historicalTicksLast(self, req_id: int, ticks: list, done: bool) -> None: + def process_historical_ticks_last(self, *, req_id: int, ticks: list, done: bool) -> None: """ Return the requested historic trade ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return self._process_trade_ticks(req_id, ticks) - def historicalTicks(self, req_id: int, ticks: list, done: bool) -> None: + def process_historical_ticks(self, *, req_id: int, ticks: list, done: bool) -> None: """ Return the requested historic ticks. """ - self.logAnswer(current_fn_name(), vars()) if not done: return self._process_trade_ticks(req_id, ticks) diff --git a/nautilus_trader/adapters/interactive_brokers/client/order.py b/nautilus_trader/adapters/interactive_brokers/client/order.py index 25961f21bead..806efecad472 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/order.py +++ b/nautilus_trader/adapters/interactive_brokers/client/order.py @@ -20,7 +20,6 @@ from ibapi.execution import Execution from ibapi.order import Order as IBOrder from ibapi.order_state import OrderState as IBOrderState -from ibapi.utils import current_fn_name from nautilus_trader.adapters.interactive_brokers.client.common import AccountOrderRef from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin @@ -143,8 +142,7 @@ def next_order_id(self) -> int: self._eclient.reqIds(-1) return order_id - # -- EWrapper overrides ----------------------------------------------------------------------- - def nextValidId(self, order_id: int) -> None: + def process_next_valid_id(self, *, order_id: int) -> None: """ Receive the next valid order id. @@ -153,14 +151,14 @@ def nextValidId(self, order_id: int) -> None: Important: the next valid order ID is only valid at the time it is received. """ - self.logAnswer(current_fn_name(), vars()) self._next_valid_order_id = max(self._next_valid_order_id, order_id, 101) if self.accounts() and not self._is_ib_ready.is_set(): self._log.info("`is_ib_ready` set by nextValidId", LogColor.BLUE) self._is_ib_ready.set() - def openOrder( + def process_open_order( self, + *, order_id: int, contract: Contract, order: IBOrder, @@ -169,7 +167,6 @@ def openOrder( """ Feed in currently open orders. """ - self.logAnswer(current_fn_name(), vars()) # Handle response to on-demand request if request := self._requests.get(name="OpenOrders"): order.contract = IBContract(**contract.__dict__) @@ -201,16 +198,16 @@ def openOrder( order_state=order_state, ) - def openOrderEnd(self) -> None: + def process_open_order_end(self) -> None: """ Notifies the end of the open orders' reception. """ - self.logAnswer(current_fn_name(), vars()) if request := self._requests.get(name="OpenOrders"): self._end_request(request.req_id) - def orderStatus( + def process_order_status( self, + *, order_id: int, status: str, filled: Decimal, @@ -229,7 +226,6 @@ def orderStatus( Note: Often there are duplicate orderStatus messages. """ - self.logAnswer(current_fn_name(), vars()) order_ref = self._order_id_to_order_ref.get(order_id, None) if order_ref: name = f"orderStatus-{order_ref.account_id}" @@ -239,8 +235,9 @@ def orderStatus( order_status=status, ) - def execDetails( + def process_exec_details( self, + *, req_id: int, contract: Contract, execution: Execution, @@ -248,7 +245,6 @@ def execDetails( """ Provide the executions that happened in the prior 24 hours. """ - self.logAnswer(current_fn_name(), vars()) if not (cache := self._exec_id_details.get(execution.execId, None)): self._exec_id_details[execution.execId] = {} cache = self._exec_id_details[execution.execId] @@ -266,14 +262,14 @@ def execDetails( ) cache.pop(execution.execId, None) - def commissionReport( + def process_commission_report( self, + *, commission_report: CommissionReport, ) -> None: """ Provide the CommissionReport of an Execution. """ - self.logAnswer(current_fn_name(), vars()) if not (cache := self._exec_id_details.get(commission_report.execId, None)): self._exec_id_details[commission_report.execId] = {} cache = self._exec_id_details[commission_report.execId] diff --git a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py new file mode 100644 index 000000000000..9e36c7d0f1f3 --- /dev/null +++ b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py @@ -0,0 +1,1239 @@ +from decimal import Decimal +from typing import TYPE_CHECKING + +from ibapi.commission_report import CommissionReport +from ibapi.common import BarData +from ibapi.common import FaDataType +from ibapi.common import HistogramData +from ibapi.common import ListOfContractDescription +from ibapi.common import ListOfDepthExchanges +from ibapi.common import ListOfFamilyCode +from ibapi.common import ListOfHistoricalSessions +from ibapi.common import ListOfHistoricalTick +from ibapi.common import ListOfHistoricalTickBidAsk +from ibapi.common import ListOfHistoricalTickLast +from ibapi.common import ListOfNewsProviders +from ibapi.common import ListOfPriceIncrements +from ibapi.common import OrderId +from ibapi.common import SetOfFloat +from ibapi.common import SetOfString +from ibapi.common import SmartComponentMap +from ibapi.common import TickAttrib +from ibapi.common import TickAttribBidAsk +from ibapi.common import TickAttribLast +from ibapi.common import TickerId +from ibapi.contract import Contract +from ibapi.contract import ContractDetails +from ibapi.contract import DeltaNeutralContract +from ibapi.execution import Execution +from ibapi.order import Order +from ibapi.order_state import OrderState +from ibapi.ticktype import TickType +from ibapi.utils import current_fn_name +from ibapi.wrapper import EWrapper + +from nautilus_trader.common.component import Logger + + +if TYPE_CHECKING: + from nautilus_trader.adapters.interactive_brokers.client.client import InteractiveBrokersClient + + +class InteractiveBrokersEWrapper(EWrapper): + def __init__( + self, + nautilus_logger: Logger, + client: "InteractiveBrokersClient", + ): + super().__init__() + self._log = nautilus_logger + self._client = client + + def logAnswer(self, fnName, fnParams): + """ + Override the logging for EWrapper.logAnswer. + """ + if "self" in fnParams: + prms = dict(fnParams) + del prms["self"] + else: + prms = fnParams + self._log.debug(f"Msg handled: function={fnName} data={prms}") + + def error(self, reqId: TickerId, errorCode: int, errorString: str, advancedOrderRejectJson=""): + """ + Call this event in response to an error in communication or when TWS needs to + send a message to the client. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_error( + req_id=reqId, + error_code=errorCode, + error_string=errorString, + advanced_order_reject_json=advancedOrderRejectJson, + ) + + def winError(self, text: str, lastError: int): + self.logAnswer(current_fn_name(), vars()) + + def connectAck(self): + """ + Invoke this callback to signify the completion of a successful connection. + """ + self.logAnswer(current_fn_name(), vars()) + + def marketDataType(self, reqId: TickerId, marketDataType: int): + """ + Receives notification when the market data type changes. + + This method is called when TWS sends a marketDataType(type) callback to the API, + where type is set to Frozen or RealTime, to announce that market data has been + switched between frozen and real-time. This notification occurs only when market + data switches between real-time and frozen. The marketDataType() callback accepts + a reqId parameter and is sent per every subscription because different contracts + can generally trade on a different schedule. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + marketDataType : int + The type of market data being received. Possible values are 1 for real-time streaming, 2 for frozen market data. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_market_data_type(req_id=reqId, market_data_type=marketDataType) + + def tickPrice( + self, + reqId: TickerId, + tickType: TickType, + price: float, + attrib: TickAttrib, + ): + """ + Market data tick price callback. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + price : float + The price of the tick. + attrib : TickAttrib + The tick's attributes. + + """ + self.logAnswer(current_fn_name(), vars()) + + def tickSize(self, reqId: TickerId, tickType: TickType, size: Decimal): + """ + Handle tick size-related market data. + + This method is responsible for handling all size-related ticks from the market data. + Each tick represents a change in the market size for a specific type of data. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + size : Decimal + The size of the tick. + + """ + self.logAnswer(current_fn_name(), vars()) + + def tickSnapshotEnd(self, reqId: int): + """ + When requesting market data snapshots, this market will indicate the snapshot + reception is finished. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickGeneric(self, reqId: TickerId, tickType: TickType, value: float): + self.logAnswer(current_fn_name(), vars()) + + def tickString(self, reqId: TickerId, tickType: TickType, value: str): + self.logAnswer(current_fn_name(), vars()) + + def tickEFP( + self, + reqId: TickerId, + tickType: TickType, + basisPoints: float, + formattedBasisPoints: str, + totalDividends: float, + holdDays: int, + futureLastTradeDate: str, + dividendImpact: float, + dividendsToLastTradeDate: float, + ): + """ + Market data callback for Exchange for Physical. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + tickType : TickType + The type of tick being received. + basisPoints : float + Annualized basis points, representative of the financing rate that can be directly be + compared to broker rates. + formattedBasisPoints : str + Annualized basis points as a formatted string depicting them in percentage form. + totalDividends : float + The total dividends. + holdDays : int + The number of hold days until the lastTradeDate of the EFP. + futureLastTradeDate : str + The expiration date of the single stock future. + dividendImpact : float + The dividend impact upon the annualized basis points interest rate. + dividendsToLastTradeDate : float + The dividends expected until the expiration of the single stock future. + + """ + self.logAnswer(current_fn_name(), vars()) + + def orderStatus( + self, + orderId: OrderId, + status: str, + filled: Decimal, + remaining: Decimal, + avgFillPrice: float, + permId: int, + parentId: int, + lastFillPrice: float, + clientId: int, + whyHeld: str, + mktCapPrice: float, + ): + """ + Call this event whenever the status of an order changes. Also, fire it after + reconnecting to TWS if the client has any open orders. + + Parameters + ---------- + orderId: OrderId + The order ID that was specified previously in the call to placeOrder(). + status: str + The order status. Possible values include: + PendingSubmit, PendingCancel, PreSubmitted, Submitted, Cancelled, Filled, Inactive. + filled: int + Specifies the number of shares that have been executed. + remaining: int + Specifies the number of shares still outstanding. + avgFillPrice: float + The average price of the shares that have been executed. + permId: int + The TWS id used to identify orders. Remains the same over TWS sessions. + parentId: int + The order ID of the parent order, used for bracket and auto trailing stop orders. + lastFillPrice: float + The last price of the shares that have been executed. + clientId: int + The ID of the client (or TWS) that placed the order. + whyHeld: str + This field is used to identify an order held when TWS is trying to locate shares for a short sell. + The value used to indicate this is 'locate'. + mktCapPrice: float + The price at which the market cap price is calculated. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_order_status( + order_id=orderId, + status=status, + filled=filled, + remaining=remaining, + avg_fill_price=avgFillPrice, + perm_id=permId, + parent_id=parentId, + last_fill_price=lastFillPrice, + client_id=clientId, + why_held=whyHeld, + mkt_cap_price=mktCapPrice, + ) + + def openOrder( + self, + orderId: OrderId, + contract: Contract, + order: Order, + orderState: OrderState, + ): + """ + Call this function to feed in open orders. + + Parameters + ---------- + orderId: OrderId + The order ID assigned by TWS. Use to cancel or update TWS order. + contract: Contract + The Contract class attributes describe the contract. + order: Order + The Order class gives the details of the open order. + orderState: OrderState + The orderState class includes attributes Used for both pre and post trade margin and commission data. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_open_order( + order_id=orderId, + contract=contract, + order=order, + order_state=orderState, + ) + + def openOrderEnd(self): + """ + Call this at the end of a given request for open orders. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_open_order_end() + + def connectionClosed(self): + """ + Call this function when TWS closes the socket connection with the ActiveX + control, or when TWS is shut down. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_connection_closed() + + def updateAccountValue( + self, + key: str, + val: str, + currency: str, + accountName: str, + ): + """ + Call this function only when ReqAccountUpdates on EEClientSocket object has been + called. + """ + self.logAnswer(current_fn_name(), vars()) + + def updatePortfolio( + self, + contract: Contract, + position: Decimal, + marketPrice: float, + marketValue: float, + averageCost: float, + unrealizedPNL: float, + realizedPNL: float, + accountName: str, + ): + """ + Call this function only when reqAccountUpdates on EEClientSocket object has been + called. + """ + self.logAnswer(current_fn_name(), vars()) + + def updateAccountTime(self, timeStamp: str): + self.logAnswer(current_fn_name(), vars()) + + def accountDownloadEnd(self, accountName: str): + """ + Call this after a batch updateAccountValue() and updatePortfolio() is sent. + """ + self.logAnswer(current_fn_name(), vars()) + + def nextValidId(self, orderId: int): + """ + Receives next valid order id. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_next_valid_id(order_id=orderId) + + def contractDetails(self, reqId: int, contractDetails: ContractDetails): + """ + Receives the full contract's definitions. + + This method will return all + contracts matching the requested via EEClientSocket::reqContractDetails. + For example, one can obtain the whole option chain with it. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_contract_details(req_id=reqId, contract_details=contractDetails) + + def bondContractDetails(self, reqId: int, contractDetails: ContractDetails): + """ + Call this function when the reqContractDetails function has been called for + bonds. + """ + self.logAnswer(current_fn_name(), vars()) + + def contractDetailsEnd(self, reqId: int): + """ + Call this function once all contract details for a given request are received. + + This helps to define the end of an option chain. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_contract_details_end(req_id=reqId) + + def execDetails(self, reqId: int, contract: Contract, execution: Execution): + """ + Fire this event when the reqExecutions() function is invoked or when an order is + filled. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_exec_details( + req_id=reqId, + contract=contract, + execution=execution, + ) + + def execDetailsEnd(self, reqId: int): + """ + Call this function once all executions have been sent to a client in response to + reqExecutions(). + """ + self.logAnswer(current_fn_name(), vars()) + + def updateMktDepth( + self, + reqId: TickerId, + position: int, + operation: int, + side: int, + price: float, + size: Decimal, + ): + """ + Return the order book. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + position : int + The order book's row being updated. + operation : int + How to refresh the row: + - 0: insert (insert this new order into the row identified by 'position') + - 1: update (update the existing order in the row identified by 'position') + - 2: delete (delete the existing order at the row identified by 'position'). + side : int + 0 for ask, 1 for bid. + price : float + The order's price. + size : Decimal + The order's size. + + """ + self.logAnswer(current_fn_name(), vars()) + + def updateMktDepthL2( + self, + reqId: TickerId, + position: int, + marketMaker: str, + operation: int, + side: int, + price: float, + size: Decimal, + isSmartDepth: bool, + ): + """ + Return the order book. + + Parameters + ---------- + reqId : TickerId + The request's identifier. + position : int + The order book's row being updated. + marketMaker : str + The exchange holding the order. + operation : int + How to refresh the row: + - 0: insert (insert this new order into the row identified by 'position') + - 1: update (update the existing order in the row identified by 'position') + - 2: delete (delete the existing order at the row identified by 'position'). + side : int + 0 for ask, 1 for bid. + price : float + The order's price. + size : Decimal + The order's size. + isSmartDepth : bool + Is SMART Depth request. + + """ + self.logAnswer(current_fn_name(), vars()) + + def updateNewsBulletin( + self, + msgId: int, + msgType: int, + newsMessage: str, + originExch: str, + ): + """ + Provide IB's bulletins. + + Parameters + ---------- + msgId: int + The bulletin's identifier. + msgType: int + One of: + - 1: Regular news bulletin + - 2: Exchange no longer available for trading + - 3: Exchange is available for trading + newsMessage: str + The message. + originExch: str + The exchange where the message comes from. + + """ + self.logAnswer(current_fn_name(), vars()) + + def managedAccounts(self, accountsList: str): + """ + Receives a comma-separated string with the managed account ids. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_managed_accounts(accounts_list=accountsList) + + def receiveFA(self, faData: FaDataType, cxml: str): + """ + Receives the Financial Advisor's configuration available in the TWS. + + Parameters + ---------- + faData : str + One of the following: + - Groups: Offer traders a way to create a group of accounts and apply a single allocation method + to all accounts in the group. + - Profiles: Let you allocate shares on an account-by-account basis using a predefined calculation value. + - Account Aliases: Let you easily identify the accounts by meaningful names rather than account numbers. + cxml : str + The XML-formatted configuration. + + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalData(self, reqId: int, bar: BarData): + """ + Return the requested historical data bars. + + Parameters + ---------- + reqId : int + The request's identifier. + bar : BarData + The bar's data. + + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalDataEnd(self, reqId: int, start: str, end: str): + """ + Mark the end of the reception of historical bars. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_data_end(req_id=reqId, start=start, end=end) + + def scannerParameters(self, xml: str): + """ + Provide the XML-formatted parameters available to create a market scanner. + + Parameters + ---------- + xml : str + The XML-formatted string with the available parameters. + + """ + self.logAnswer(current_fn_name(), vars()) + + def scannerData( + self, + reqId: int, + rank: int, + contractDetails: ContractDetails, + distance: str, + benchmark: str, + projection: str, + legsStr: str, + ): + """ + Provide the data resulting from the market scanner request. + + Parameters + ---------- + reqId : int + The request's identifier. + rank : int + The ranking within the response of this bar. + contractDetails : ContractDetails + The data's ContractDetails. + distance : str + According to query. + benchmark : str + According to query. + projection : str + According to query. + legsStr : str + Describes the combo legs when the scanner is returning EFP. + + """ + self.logAnswer(current_fn_name(), vars()) + + def scannerDataEnd(self, reqId: int): + """ + Indicate that scanner data reception has terminated. + + Parameters + ---------- + reqId : int + The request's identifier. + + """ + self.logAnswer(current_fn_name(), vars()) + + def realtimeBar( + self, + reqId: TickerId, + time: int, + open_: float, + high: float, + low: float, + close: float, + volume: Decimal, + wap: Decimal, + count: int, + ): + """ + Update real-time 5-second bars. + + Parameters + ---------- + reqId : int + The request's identifier. + time : int + Start of the bar in Unix (or 'epoch') time. + open_ : float + The bar's open value. + high : float + The bar's high value. + low : float + The bar's low value. + close : float + The bar's closing value. + volume : int + The bar's traded volume if available. + wap : float + The bar's Weighted Average Price. + count : int + The number of trades during the bar's timespan (only available for TRADES). + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_realtime_bar( + req_id=reqId, + time=time, + open_=open_, + high=high, + low=low, + close=close, + volume=volume, + wap=wap, + count=count, + ) + + def currentTime(self, time: int): + """ + Obtain the IB server's system time by calling this method as a result of + invoking `reqCurrentTime`. + """ + self.logAnswer(current_fn_name(), vars()) + + def fundamentalData(self, reqId: TickerId, data: str): + """ + Call this function to receive fundamental market data. + + Ensure that the appropriate market data subscription is set up in Account + Management before attempting to receive this data. + + """ + self.logAnswer(current_fn_name(), vars()) + + def deltaNeutralValidation(self, reqId: int, deltaNeutralContract: DeltaNeutralContract): + """ + When accepting a Delta-Neutral RFQ (request for quote), the server sends a + deltaNeutralValidation() message with the DeltaNeutralContract structure. + + If the delta and price fields are empty in the original request, the + confirmation will contain the current values from the server. These values are + locked when the RFQ is processed and remain locked until the RFQ is canceled. + + """ + self.logAnswer(current_fn_name(), vars()) + + def commissionReport(self, commissionReport: CommissionReport): + """ + Trigger this callback in the following scenarios: + + - Immediately after a trade execution. + - By calling reqExecutions(). + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_commission_report(commission_report=commissionReport) + + def position( + self, + account: str, + contract: Contract, + position: Decimal, + avgCost: float, + ): + """ + Return real-time positions for all accounts in response to the reqPositions() + method. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_position( + account_id=account, + contract=contract, + position=position, + avg_cost=avgCost, + ) + + def positionEnd(self): + """ + Call this once all position data for a given request has been received, serving + as an end marker for the position() data. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_position_end() + + def accountSummary( + self, + reqId: int, + account: str, + tag: str, + value: str, + currency: str, + ): + """ + Return the data from the TWS Account Window Summary tab in response to + reqAccountSummary(). + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_account_summary( + req_id=reqId, + account_id=account, + tag=tag, + value=value, + currency=currency, + ) + + def accountSummaryEnd(self, reqId: int): + """ + Call this method when all account summary data for a given request has been + received. + """ + self.logAnswer(current_fn_name(), vars()) + + def verifyCompleted(self, isSuccessful: bool, errorText: str): + + self.logAnswer(current_fn_name(), vars()) + + def verifyAndAuthMessageAPI(self, apiData: str, xyzChallange: str): + + self.logAnswer(current_fn_name(), vars()) + + def verifyAndAuthCompleted(self, isSuccessful: bool, errorText: str): + + self.logAnswer(current_fn_name(), vars()) + + def displayGroupList(self, reqId: int, groups: str): + """ + Receive a one-time response callback to queryDisplayGroups(). + + Parameters + ---------- + reqId : int + The requestId specified in queryDisplayGroups(). + groups : str + A list of integers representing visible group IDs separated by the '|' character, sorted by most + used group first. This list remains unchanged during the TWS session (i.e., users cannot add new + groups; sorting can change). + + """ + self.logAnswer(current_fn_name(), vars()) + + def displayGroupUpdated(self, reqId: int, contractInfo: str): + """ + Receive a notification from TWS to the API client after subscribing to group + events via subscribeToGroupEvents(). This notification will be resent if the + chosen contract in the subscribed display group changes. + + Parameters + ---------- + reqId : int + The requestId specified in subscribeToGroupEvents(). + contractInfo : str + The encoded value uniquely representing the contract in IB. Possible values include: + - 'none': Empty selection. + - 'contractID@exchange': For any non-combination contract. + Examples: '8314@SMART' for IBM SMART; '8314@ARCA' for IBM @ARCA. + - 'combo': If any combo is selected. + + """ + self.logAnswer(current_fn_name(), vars()) + + def positionMulti( + self, + reqId: int, + account: str, + modelCode: str, + contract: Contract, + pos: Decimal, + avgCost: float, + ): + """ + Retrieve the position for a specific account or model, mirroring the position() + function. + """ + self.logAnswer(current_fn_name(), vars()) + + def positionMultiEnd(self, reqId: int): + """ + Terminate the position for a specific account or model, akin to the + positionEnd() function. + """ + self.logAnswer(current_fn_name(), vars()) + + def accountUpdateMulti( + self, + reqId: int, + account: str, + modelCode: str, + key: str, + value: str, + currency: str, + ): + """ + Update the value for a specific account or model, similar to the + updateAccountValue() function. + """ + self.logAnswer(current_fn_name(), vars()) + + def accountUpdateMultiEnd(self, reqId: int): + """ + Download data for a specific account or model, resembling accountDownloadEnd() + functionality. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickOptionComputation( + self, + reqId: TickerId, + tickType: TickType, + tickAttrib: int, + impliedVol: float, + delta: float, + optPrice: float, + pvDividend: float, + gamma: float, + vega: float, + theta: float, + undPrice: float, + ): + """ + Invoke this function in response to market movements in an option or its + underlier. + + Receive TWS's option model volatilities, prices, and deltas, as well as the + present value of dividends expected on the option's underlier. + + """ + self.logAnswer(current_fn_name(), vars()) + + def securityDefinitionOptionParameter( + self, + reqId: int, + exchange: str, + underlyingConId: int, + tradingClass: str, + multiplier: str, + expirations: SetOfString, + strikes: SetOfFloat, + ): + """ + Return the option chain for an underlying on a specified exchange. + + This is triggered by a call to `reqSecDefOptParams`. If multiple exchanges are specified in + `reqSecDefOptParams`, there will be multiple callbacks to `securityDefinitionOptionParameter`. + + Parameters + ---------- + reqId : int + ID of the request that initiated the callback. + exchange : str + The exchange for which the option chain is requested. + underlyingConId : int + The conID of the underlying security. + tradingClass : str + The option trading class. + multiplier : str + The option multiplier. + expirations : list[str] + A list of expiry dates for the options of this underlying on this exchange. + strikes : list[float] + A list of possible strikes for options of this underlying on this exchange. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_security_definition_option_parameter( + req_id=reqId, + exchange=exchange, + underlying_con_id=underlyingConId, + trading_class=tradingClass, + multiplier=multiplier, + expirations=expirations, + strikes=strikes, + ) + + def securityDefinitionOptionParameterEnd(self, reqId: int): + """ + Invoke this after all callbacks to securityDefinitionOptionParameter have been + completed. + + Parameters + ---------- + reqId : int + The ID used in the initial call to `securityDefinitionOptionParameter`. + + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_security_definition_option_parameter_end(req_id=reqId) + + def softDollarTiers(self, reqId: int, tiers: list): + """ + Invoke this upon receiving Soft Dollar Tier configuration information. + + Call this method when Soft Dollar Tier configuration details are received. + + Parameters + ---------- + reqId : int + The request ID used in the call to `EEClient::reqSoftDollarTiers`. + tiers : list[SoftDollarTier] + A list containing all Soft Dollar Tier information. + + """ + self.logAnswer(current_fn_name(), vars()) + + def familyCodes(self, familyCodes: ListOfFamilyCode): + """ + Return an array of family codes. + """ + self.logAnswer(current_fn_name(), vars()) + + def symbolSamples( + self, + reqId: int, + contractDescriptions: ListOfContractDescription, + ): + """ + Return an array of sample contract descriptions. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_symbol_samples( + req_id=reqId, + contract_descriptions=contractDescriptions, + ) + + def mktDepthExchanges(self, depthMktDataDescriptions: ListOfDepthExchanges): + """ + Return an array of exchanges that provide depth data to UpdateMktDepthL2. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickNews( + self, + tickerId: int, + timeStamp: int, + providerCode: str, + articleId: str, + headline: str, + extraData: str, + ): + """ + Return news headlines. + """ + self.logAnswer(current_fn_name(), vars()) + + def smartComponents(self, reqId: int, smartComponentMap: SmartComponentMap): + """ + Return exchange component mapping. + """ + self.logAnswer(current_fn_name(), vars()) + + def tickReqParams( + self, + tickerId: int, + minTick: float, + bboExchange: str, + snapshotPermissions: int, + ): + """ + Return the exchange map for a specific contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def newsProviders(self, newsProviders: ListOfNewsProviders): + """ + Return available and subscribed API news providers. + """ + self.logAnswer(current_fn_name(), vars()) + + def newsArticle(self, requestId: int, articleType: int, articleText: str): + """ + Return the body of a news article. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalNews( + self, + requestId: int, + time: str, + providerCode: str, + articleId: str, + headline: str, + ): + """ + Return historical news headlines. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalNewsEnd(self, requestId: int, hasMore: bool): + """ + Signals end of historical news. + """ + self.logAnswer(current_fn_name(), vars()) + + def headTimestamp(self, reqId: int, headTimestamp: str): + """ + Return the earliest available data for a specific type of data for a given + contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def histogramData(self, reqId: int, items: HistogramData): + """ + Return histogram data for a contract. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalDataUpdate(self, reqId: int, bar: BarData): + """ + Return updates in real time when keepUpToDate is set to True. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_data_update( + req_id=reqId, + bar=bar, + ) + + def rerouteMktDataReq(self, reqId: int, conId: int, exchange: str): + """ + Return rerouted CFD contract information for a market data request. + """ + self.logAnswer(current_fn_name(), vars()) + + def rerouteMktDepthReq(self, reqId: int, conId: int, exchange: str): + """ + Return rerouted CFD contract information for a market depth request. + """ + self.logAnswer(current_fn_name(), vars()) + + def marketRule(self, marketRuleId: int, priceIncrements: ListOfPriceIncrements): + """ + Return the minimum price increment structure for a specific market rule ID. + """ + self.logAnswer(current_fn_name(), vars()) + + def pnl(self, reqId: int, dailyPnL: float, unrealizedPnL: float, realizedPnL: float): + """ + Return the daily Profit and Loss (PnL) for the account. + """ + self.logAnswer(current_fn_name(), vars()) + + def pnlSingle( + self, + reqId: int, + pos: Decimal, + dailyPnL: float, + unrealizedPnL: float, + realizedPnL: float, + value: float, + ): + """ + Return the daily Profit and Loss (PnL) for a single position in the account. + """ + self.logAnswer(current_fn_name(), vars()) + + def historicalTicks(self, reqId: int, ticks: ListOfHistoricalTick, done: bool): + """ + Return historical tick data when whatToShow is set to MIDPOINT. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def historicalTicksBidAsk(self, reqId: int, ticks: ListOfHistoricalTickBidAsk, done: bool): + """ + Return historical tick data when whatToShow is set to BID_ASK. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks_bid_ask( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def historicalTicksLast(self, reqId: int, ticks: ListOfHistoricalTickLast, done: bool): + """ + Return historical tick data when whatToShow is set to TRADES. + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_historical_ticks_last( + req_id=reqId, + ticks=ticks, + done=done, + ) + + def tickByTickAllLast( + self, + reqId: int, + tickType: int, + time: int, + price: float, + size: Decimal, + tickAttribLast: TickAttribLast, + exchange: str, + specialConditions: str, + ): + """ + Return tick-by-tick data for tickType set to "Last" or "AllLast". + """ + self.logAnswer(current_fn_name(), vars()) + self._process_tick_by_tick_all_last( + req_id=reqId, + tick_type=tickType, + time=time, + price=price, + size=size, + tick_attrib_last=tickAttribLast, + exchange=exchange, + special_conditions=specialConditions, + ) + + def tickByTickBidAsk( + self, + reqId: int, + time: int, + bidPrice: float, + askPrice: float, + bidSize: Decimal, + askSize: Decimal, + tickAttribBidAsk: TickAttribBidAsk, + ): + """ + Return tick-by-tick data for tickType set to "BidAsk". + """ + self.logAnswer(current_fn_name(), vars()) + self._client.process_tick_by_tick_bid_ask( + req_id=reqId, + time=time, + bid_price=bidPrice, + ask_price=askPrice, + bid_size=bidSize, + ask_size=askSize, + tick_attrib_bid_ask=tickAttribBidAsk, + ) + + def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): + """ + Return tick-by-tick data for tickType set to "MidPoint". + """ + self.logAnswer(current_fn_name(), vars()) + + def orderBound(self, reqId: int, apiClientId: int, apiOrderId: int): + """ + Return the orderBound notification. + """ + self.logAnswer(current_fn_name(), vars()) + + def completedOrder(self, contract: Contract, order: Order, orderState: OrderState): + """ + Feed in completed orders. + + Call this function to provide information on completed orders. + + Parameters + ---------- + contract : Contract + Describes the contract with attributes of the Contract class. + order : Order + Details of the completed order, as defined by the Order class. + orderState : OrderState + Includes status details of the completed order, as specified in the OrderState class. + + """ + self.logAnswer(current_fn_name(), vars()) + + def completedOrdersEnd(self): + """ + Invoke this upon completing a request for completed orders. + """ + self.logAnswer(current_fn_name(), vars()) + + def replaceFAEnd(self, reqId: int, text: str): + """ + Invoke this at the completion of a Financial Advisor (FA) replacement operation. + """ + self.logAnswer(current_fn_name(), vars()) + + def wshMetaData(self, reqId: int, dataJson: str): + self.logAnswer(current_fn_name(), vars()) + + def wshEventData(self, reqId: int, dataJson: str): + self.logAnswer(current_fn_name(), vars()) + + def historicalSchedule( + self, + reqId: int, + startDateTime: str, + endDateTime: str, + timeZone: str, + sessions: ListOfHistoricalSessions, + ): + """ + Return historical schedule for historical data request with whatToShow=SCHEDULE. + """ + self.logAnswer(current_fn_name(), vars()) + + def userInfo(self, reqId: int, whiteBrandingId: str): + """ + Return user info. + """ + self.logAnswer(current_fn_name(), vars()) diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py index 7949cf2760f7..573a331d29c5 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py @@ -22,10 +22,10 @@ def test_ib_is_ready_by_notification_1101(ib_client): ib_client._is_ib_ready.clear() # Act - ib_client.error( - -1, - 1101, - "Connectivity between IB and Trader Workstation has been restored", + ib_client.process_error( + req_id=-1, + error_code=1101, + error_string="Connectivity between IB and Trader Workstation has been restored", ) # Assert @@ -37,10 +37,10 @@ def test_ib_is_ready_by_notification_1102(ib_client): ib_client._is_ib_ready.clear() # Act - ib_client.error( - -1, - 1102, - "Connectivity between IB and Trader Workstation has been restored", + ib_client.process_error( + req_id=-1, + error_code=1102, + error_string="Connectivity between IB and Trader Workstation has been restored", ) # Assert @@ -54,7 +54,11 @@ def test_ib_is_not_ready_by_error_10182(ib_client): ib_client._subscriptions.add(req_id, "EUR.USD", ib_client._eclient.reqHistoricalData, {}) # Act - ib_client.error(req_id, 10182, "Failed to request live updates (disconnected).") + ib_client.process_error( + req_id=req_id, + error_code=10182, + error_string="Failed to request live updates (disconnected).", + ) # Assert assert not ib_client._is_ib_ready.is_set() @@ -80,10 +84,10 @@ def test_ib_is_not_ready_by_error_10189(ib_client): ) # Act - ib_client.error( - req_id, - 10189, - "Failed to request tick-by-tick data.BidAsk tick-by-tick requests are not supported for EUR.USD.", + ib_client.process_error( + req_id=req_id, + error_code=10189, + error_string="Failed to request tick-by-tick data.BidAsk tick-by-tick requests are not supported for EUR.USD.", ) # Assert diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py index f757b2b9bb80..1d30943fe701 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_market_data.py @@ -420,14 +420,14 @@ def test_tickByTickBidAsk(ib_client): ib_client._handle_data = Mock() # Act - ib_client.tickByTickBidAsk( - 1, - 1704067200, - 100.01, - 100.02, - Decimal(100), - Decimal(200), - TickAttribBidAsk(), + ib_client.process_tick_by_tick_bid_ask( + req_id=1, + time=1704067200, + bid_price=100.01, + ask_price=100.02, + bid_size=Decimal(100), + ask_size=Decimal(200), + tick_attrib_bid_ask=TickAttribBidAsk(), ) # Assert @@ -453,15 +453,15 @@ def test_tickByTickAllLast(ib_client): ib_client._handle_data = Mock() # Act - ib_client.tickByTickAllLast( - 1, - "Last", - 1704067200, - 100.01, - Decimal(100), - TickAttribLast(), - "", - "", + ib_client.process_tick_by_tick_all_last( + req_id=1, + tick_type="Last", + time=1704067200, + price=100.01, + size=Decimal(100), + tick_attrib_last=TickAttribLast(), + exchange="", + special_conditions="", ) # Assert @@ -488,16 +488,16 @@ def test_realtimeBar(ib_client): ib_client._handle_data = Mock() # Act - ib_client.realtimeBar( - 1, - 1704067200, - 100.01, - 101.00, - 99.01, - 100.50, - Decimal(100), - Decimal(-1), - Decimal(-1), + ib_client.process_realtime_bar( + req_id=1, + time=1704067200, + open_=100.01, + high=101.00, + low=99.01, + close=100.50, + volume=Decimal(100), + wap=Decimal(-1), + count=Decimal(-1), ) # Assert diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py index 32aa56071cb3..f496e7eb2150 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_order.py @@ -119,11 +119,11 @@ def test_openOrder(ib_client): order_state = IBTestExecStubs.ib_order_state(state="PreSubmitted") # Act - ib_client.openOrder( - order_id, - contract, - order, - order_state, + ib_client.process_open_order( + order_id=order_id, + contract=contract, + order=order, + order_state=order_state, ) # Assert @@ -142,18 +142,18 @@ def test_orderStatus(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.orderStatus( - 1, - "Filled", - Decimal("100"), - Decimal("0"), - 100.0, - 1916994655, - 0, - 100.0, - 1, - "", - 0.0, + ib_client.process_order_status( + order_id=1, + status="Filled", + filled=Decimal("100"), + remaining=Decimal("0"), + avg_fill_price=100.0, + perm_id=1916994655, + parent_id=0, + last_fill_price=100.0, + client_id=1, + why_held="", + mkt_cap_price=0.0, ) # Assert @@ -188,10 +188,10 @@ def test_execDetails(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.execDetails( - req_id, - contract, - execution, + ib_client.process_exec_details( + req_id=req_id, + contract=contract, + execution=execution, ) # Assert @@ -223,7 +223,7 @@ def test_commissionReport(ib_client): ib_client._event_subscriptions.get = MagicMock(return_value=handler_func) # Act - ib_client.commissionReport(commission_report) + ib_client.process_commission_report(commission_report=commission_report) # Assert handler_func.assert_called_with( diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index 7df3e0187bcf..a29d79aded6b 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -144,8 +144,8 @@ def exec_client(mocker, exec_client_config, venue, loop, msgbus, cache, clock): clock=clock, ) client._client.start() - client._client.managedAccounts("DU123456,") - client._client.nextValidId(1) + client._client.process_managed_accounts(accounts_list="DU123456,") + client._client.process_next_valid_id(order_id=1) return client From fbef93aa90078a538292ee60397644a2557ffae7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 13:15:06 +1100 Subject: [PATCH 27/71] Update release notes --- RELEASES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 08299fa9d4b0..ff74f69b10e2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,9 +3,11 @@ Released on TBD (UTC). ### Enhancements +- Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging -- Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection +- Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton +- Refactored `InteractiveBrokersEWrapper`, thanks @rsmb7z - Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies ### Breaking Changes From 6b4fddba35da664821b8c104aa1cc244a4c991f6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 13:53:17 +1100 Subject: [PATCH 28/71] Add Rust Instrument validations --- nautilus_core/core/src/correctness.rs | 18 ++++++++++++++++-- .../model/src/instruments/crypto_future.rs | 15 ++++++++++----- .../model/src/instruments/crypto_perpetual.rs | 15 ++++++++++----- .../model/src/instruments/currency_pair.rs | 15 ++++++++++----- nautilus_core/model/src/instruments/equity.rs | 9 +++++---- .../model/src/instruments/futures_contract.rs | 14 +++++++++++--- .../model/src/instruments/futures_spread.rs | 15 ++++++++++++--- .../model/src/instruments/options_contract.rs | 14 +++++++++++--- .../model/src/instruments/options_spread.rs | 15 ++++++++++++--- 9 files changed, 97 insertions(+), 33 deletions(-) diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 90f065913d5a..72104791ecaa 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -15,9 +15,9 @@ const FAILED: &str = "Condition failed:"; -/// Validates the string `s` contains only ASCII characters and has symantic meaning. +/// Validates the string `s` contains only ASCII characters and has semantic meaning. /// -/// # Panics +/// # Errors /// /// - If `s` is an empty string. /// - If `s` consists solely of whitespace characters. @@ -34,6 +34,20 @@ pub fn check_valid_string(s: &str, param: &str) -> anyhow::Result<()> { } } +/// Validates the string `s` if Some, contains only ASCII characters and has semantic meaning. +/// +/// # Errors +/// +/// - If `s` is an empty string. +/// - If `s` consists solely of whitespace characters. +/// - If `s` contains one or more non-ASCII characters. +pub fn check_valid_string_optional(s: Option<&str>, param: &str) -> anyhow::Result<()> { + if let Some(s) = s { + check_valid_string(s, param)?; + } + Ok(()) +} + /// Validates the string `s` contains the pattern `pat`. pub fn check_string_contains(s: &str, pat: &str, param: &str) -> anyhow::Result<()> { if !s.contains(pat) { diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index 32eb4de2effb..bd3e5f9f480e 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -18,7 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -94,15 +97,17 @@ impl CryptoFuture { check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; check_equal_u8( size_precision, size_increment.precision, - "size_precision", - "size_increment.precision", + stringify!(size_precision), + stringify!(size_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/crypto_perpetual.rs b/nautilus_core/model/src/instruments/crypto_perpetual.rs index 07d46fe6ce1a..bef7d05075ec 100644 --- a/nautilus_core/model/src/instruments/crypto_perpetual.rs +++ b/nautilus_core/model/src/instruments/crypto_perpetual.rs @@ -18,7 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -92,15 +95,17 @@ impl CryptoPerpetual { check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; check_equal_u8( size_precision, size_increment.precision, - "size_precision", - "size_increment.precision", + stringify!(size_precision), + stringify!(size_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index d804275698e7..afc89b8c51fc 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -18,7 +18,10 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{check_equal_u8, check_positive_i64, check_positive_u64}, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -88,15 +91,17 @@ impl CurrencyPair { check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; check_equal_u8( size_precision, size_increment.precision, - "size_precision", - "size_increment.precision", + stringify!(size_precision), + stringify!(size_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; + check_positive_u64(size_increment.raw, stringify!(size_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 16352b435c95..796274223557 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -19,7 +19,7 @@ use std::{ }; use nautilus_core::{ - correctness::{check_equal_u8, check_positive_i64}, + correctness::{check_equal_u8, check_positive_i64, check_valid_string_optional}, time::UnixNanos, }; use rust_decimal::Decimal; @@ -82,13 +82,14 @@ impl Equity { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { + check_valid_string_optional(isin.map(|u| u.as_str()), stringify!(isin))?; check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; - check_positive_i64(price_increment.raw, "price_increment.raw")?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index 278435ff9596..3a33311b2c85 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -18,7 +18,12 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -87,12 +92,15 @@ impl FuturesContract { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/futures_spread.rs b/nautilus_core/model/src/instruments/futures_spread.rs index 4c4ecad83fc0..5bf71d87dfd2 100644 --- a/nautilus_core/model/src/instruments/futures_spread.rs +++ b/nautilus_core/model/src/instruments/futures_spread.rs @@ -18,7 +18,12 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -89,12 +94,16 @@ impl FuturesSpread { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_valid_string(strategy_type.as_str(), stringify!(strategy_type))?; check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index f784b002ee10..ea1f0a55cad3 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -18,7 +18,12 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -91,12 +96,15 @@ impl OptionsContract { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; Ok(Self { id, diff --git a/nautilus_core/model/src/instruments/options_spread.rs b/nautilus_core/model/src/instruments/options_spread.rs index 8663d5636e75..4aad434cfeb2 100644 --- a/nautilus_core/model/src/instruments/options_spread.rs +++ b/nautilus_core/model/src/instruments/options_spread.rs @@ -18,7 +18,12 @@ use std::{ hash::{Hash, Hasher}, }; -use nautilus_core::{correctness::check_equal_u8, time::UnixNanos}; +use nautilus_core::{ + correctness::{ + check_equal_u8, check_positive_i64, check_valid_string, check_valid_string_optional, + }, + time::UnixNanos, +}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -89,12 +94,16 @@ impl OptionsSpread { ts_event: UnixNanos, ts_init: UnixNanos, ) -> anyhow::Result { + check_valid_string_optional(exchange.map(|u| u.as_str()), stringify!(isin))?; + check_valid_string(underlying.as_str(), stringify!(underlying))?; + check_valid_string(strategy_type.as_str(), stringify!(strategy_type))?; check_equal_u8( price_precision, price_increment.precision, - "price_precision", - "price_increment.precision", + stringify!(price_precision), + stringify!(price_increment.precision), )?; + check_positive_i64(price_increment.raw, stringify!(price_increment.raw))?; Ok(Self { id, From 0f51b50bb4170eacee19f01ecec5459e75eeec9e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 14:44:27 +1100 Subject: [PATCH 29/71] Add Rust Cache scaffolding --- nautilus_core/backtest/src/matching_engine.rs | 2 +- nautilus_core/common/src/cache.rs | 157 ++++++++++++++++++ nautilus_core/common/src/lib.rs | 1 + nautilus_core/infrastructure/src/cache.rs | 75 --------- nautilus_core/infrastructure/src/lib.rs | 2 - .../infrastructure/src/python/cache.rs | 3 +- nautilus_core/infrastructure/src/redis.rs | 7 +- 7 files changed, 165 insertions(+), 82 deletions(-) create mode 100644 nautilus_core/common/src/cache.rs delete mode 100644 nautilus_core/infrastructure/src/cache.rs diff --git a/nautilus_core/backtest/src/matching_engine.rs b/nautilus_core/backtest/src/matching_engine.rs index abdfc3ab3871..b0c51c4fee7d 100644 --- a/nautilus_core/backtest/src/matching_engine.rs +++ b/nautilus_core/backtest/src/matching_engine.rs @@ -48,7 +48,7 @@ pub struct OrderMatchingEngine { pub account_type: AccountType, pub market_status: MarketStatus, pub config: OrderMatchingEngineConfig, - // pub cache: Cache // TODO! + // pub cache: Cache // TODO clock: &'static AtomicTime, msgbus: &'static MessageBus, book_mbo: Option, diff --git a/nautilus_core/common/src/cache.rs b/nautilus_core/common/src/cache.rs new file mode 100644 index 000000000000..18893356eda7 --- /dev/null +++ b/nautilus_core/common/src/cache.rs @@ -0,0 +1,157 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +#![allow(dead_code)] // Under development + +use std::{ + collections::{HashMap, HashSet, VecDeque}, + sync::mpsc::Receiver, +}; + +use nautilus_core::uuid::UUID4; +use nautilus_model::{ + data::{ + bar::{Bar, BarType}, + quote::QuoteTick, + trade::TradeTick, + }, + identifiers::{ + account_id::AccountId, client_id::ClientId, client_order_id::ClientOrderId, + component_id::ComponentId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, + position_id::PositionId, strategy_id::StrategyId, symbol::Symbol, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, + }, + instruments::{synthetic::SyntheticInstrument, Instrument}, + orders::base::Order, + position::Position, + types::currency::Currency, +}; +use ustr::Ustr; + +/// A type of database operation. +#[derive(Clone, Debug)] +pub enum DatabaseOperation { + Insert, + Update, + Delete, +} + +/// Represents a database command to be performed which may be executed 'remotely' across a thread. +#[derive(Clone, Debug)] +pub struct DatabaseCommand { + /// The database operation type. + pub op_type: DatabaseOperation, + /// The primary key for the operation. + pub key: String, + /// The data payload for the operation. + pub payload: Option>>, +} + +impl DatabaseCommand { + pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { + Self { + op_type, + key, + payload, + } + } +} + +/// Provides a generic cache database facade. +/// +/// The main operations take a consistent `key` and `payload` which should provide enough +/// information to implement the cache database in many different technologies. +/// +/// Delete operations may need a `payload` to target specific values. +pub trait CacheDatabase { + type DatabaseType; + + fn new( + trader_id: TraderId, + instance_id: UUID4, + config: HashMap, + ) -> anyhow::Result; + fn flushdb(&mut self) -> anyhow::Result<()>; + fn keys(&mut self, pattern: &str) -> anyhow::Result>; + fn read(&mut self, key: &str) -> anyhow::Result>>; + fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; + fn handle_messages( + rx: Receiver, + trader_key: String, + config: HashMap, + ); +} + +pub struct CacheConfig { + pub tick_capacity: usize, + pub bar_capacity: usize, + pub snapshot_orders: bool, + pub snapshot_positions: bool, +} + +pub struct CacheIndex { + venue_account: HashMap, + venue_orders: HashMap>, + venue_positions: HashMap>, + order_ids: HashMap, + order_position: HashMap, + order_strategy: HashMap, + order_client: HashMap, + position_strategy: HashMap, + position_orders: HashMap>, + instrument_orders: HashMap>, + instrument_positions: HashMap>, + strategy_orders: HashMap>, + strategy_positions: HashMap>, + exec_algorithm_orders: HashMap>, + exec_spawn_orders: HashMap>, + orders: HashSet, + orders_open: HashSet, + orders_closed: HashSet, + orders_emulated: HashSet, + orders_inflight: HashSet, + orders_pending_cancel: HashSet, + positions: HashSet, + positions_open: HashSet, + positions_closed: HashSet, + actors: HashSet, + strategies: HashSet, + exec_algorithms: HashSet, +} + +pub struct Cache { + config: CacheConfig, + index: CacheIndex, + // database: Option>, TODO + // xrate_calculator: ExchangeRateCalculator TODO + general: HashMap>, + xrate_symbols: HashMap, + quote_ticks: HashMap>, + trade_ticks: HashMap>, + // order_books: HashMap>, TODO: Needs single book + bars: HashMap>, + bars_bid: HashMap, + bars_ask: HashMap, + currencies: HashMap, + instruments: HashMap>, + synthetics: HashMap, + // accounts: HashMap>, TODO: Decide where trait should go + orders: HashMap>>, // TODO: Efficency (use enum) + // order_lists: HashMap>, TODO: Need `OrderList` + positions: HashMap, + position_snapshots: HashMap>, +} diff --git a/nautilus_core/common/src/lib.rs b/nautilus_core/common/src/lib.rs index 4f77acc139a0..bc8cbb6ef5b1 100644 --- a/nautilus_core/common/src/lib.rs +++ b/nautilus_core/common/src/lib.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod cache; pub mod clock; pub mod enums; pub mod factories; diff --git a/nautilus_core/infrastructure/src/cache.rs b/nautilus_core/infrastructure/src/cache.rs deleted file mode 100644 index 452b4d1ec00b..000000000000 --- a/nautilus_core/infrastructure/src/cache.rs +++ /dev/null @@ -1,75 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// 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::{collections::HashMap, sync::mpsc::Receiver}; - -use nautilus_core::uuid::UUID4; -use nautilus_model::identifiers::trader_id::TraderId; - -/// A type of database operation. -#[derive(Clone, Debug)] -pub enum DatabaseOperation { - Insert, - Update, - Delete, -} - -/// Represents a database command to be performed which may be executed 'remotely' across a thread. -#[derive(Clone, Debug)] -pub struct DatabaseCommand { - /// The database operation type. - pub op_type: DatabaseOperation, - /// The primary key for the operation. - pub key: String, - /// The data payload for the operation. - pub payload: Option>>, -} - -impl DatabaseCommand { - pub fn new(op_type: DatabaseOperation, key: String, payload: Option>>) -> Self { - Self { - op_type, - key, - payload, - } - } -} - -/// Provides a generic cache database facade. -/// -/// The main operations take a consistent `key` and `payload` which should provide enough -/// information to implement the cache database in many different technologies. -/// -/// Delete operations may need a `payload` to target specific values. -pub trait CacheDatabase { - type DatabaseType; - - fn new( - trader_id: TraderId, - instance_id: UUID4, - config: HashMap, - ) -> anyhow::Result; - fn flushdb(&mut self) -> anyhow::Result<()>; - fn keys(&mut self, pattern: &str) -> anyhow::Result>; - fn read(&mut self, key: &str) -> anyhow::Result>>; - fn insert(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; - fn update(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; - fn delete(&mut self, key: String, payload: Option>>) -> anyhow::Result<()>; - fn handle_messages( - rx: Receiver, - trader_key: String, - config: HashMap, - ); -} diff --git a/nautilus_core/infrastructure/src/lib.rs b/nautilus_core/infrastructure/src/lib.rs index a05090fafc7d..ae05731698d4 100644 --- a/nautilus_core/infrastructure/src/lib.rs +++ b/nautilus_core/infrastructure/src/lib.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -pub mod cache; - #[cfg(feature = "python")] pub mod python; diff --git a/nautilus_core/infrastructure/src/python/cache.rs b/nautilus_core/infrastructure/src/python/cache.rs index 25d74b6b3d34..e8f86bcf622e 100644 --- a/nautilus_core/infrastructure/src/python/cache.rs +++ b/nautilus_core/infrastructure/src/python/cache.rs @@ -15,6 +15,7 @@ use std::collections::HashMap; +use nautilus_common::cache::CacheDatabase; use nautilus_core::{ python::{to_pyruntime_err, to_pyvalue_err}, uuid::UUID4, @@ -22,7 +23,7 @@ use nautilus_core::{ use nautilus_model::identifiers::trader_id::TraderId; use pyo3::{prelude::*, types::PyBytes}; -use crate::{cache::CacheDatabase, redis::RedisCacheDatabase}; +use crate::redis::RedisCacheDatabase; #[pymethods] impl RedisCacheDatabase { diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 8ee0c02eee94..4ef644c3f63f 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -20,15 +20,16 @@ use std::{ time::{Duration, Instant}, }; -use nautilus_common::redis::{get_buffer_interval, get_redis_url, get_timeout_duration}; +use nautilus_common::{ + cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}, + redis::{get_buffer_interval, get_redis_url, get_timeout_duration}, +}; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::{Commands, Connection, Pipeline}; use serde_json::json; use tracing::debug; -use crate::cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}; - // Error constants const CHANNEL_TX_FAILED: &str = "Failed to send to channel"; From 58538d0df0991146f321084fe2df2b24a2f8cdad Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 14:53:38 +1100 Subject: [PATCH 30/71] Standardize cargo manifest feature flags --- nautilus_core/accounting/Cargo.toml | 2 +- nautilus_core/adapters/Cargo.toml | 2 +- nautilus_core/backtest/Cargo.toml | 2 +- nautilus_core/common/Cargo.toml | 2 +- nautilus_core/core/Cargo.toml | 3 +-- nautilus_core/execution/Cargo.toml | 2 +- nautilus_core/indicators/Cargo.toml | 2 +- nautilus_core/infrastructure/Cargo.toml | 2 +- nautilus_core/model/Cargo.toml | 4 ++-- nautilus_core/network/Cargo.toml | 2 +- nautilus_core/persistence/Cargo.toml | 2 +- nautilus_core/pyo3/Cargo.toml | 2 +- 12 files changed, 13 insertions(+), 14 deletions(-) diff --git a/nautilus_core/accounting/Cargo.toml b/nautilus_core/accounting/Cargo.toml index 4acac713c058..f81d62c9e488 100644 --- a/nautilus_core/accounting/Cargo.toml +++ b/nautilus_core/accounting/Cargo.toml @@ -27,6 +27,7 @@ rstest = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -34,4 +35,3 @@ extension-module = [ "nautilus-common/extension-module", ] python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] -default = [] diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 6e3f1a343f3a..92afa3857f9e 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -45,6 +45,7 @@ criterion = { workspace = true } rstest = { workspace = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -64,4 +65,3 @@ python = [ "nautilus-core/python", "nautilus-model/python", ] -default = ["ffi", "python"] diff --git a/nautilus_core/backtest/Cargo.toml b/nautilus_core/backtest/Cargo.toml index 6e18d3664dbc..dd51d23fdefb 100644 --- a/nautilus_core/backtest/Cargo.toml +++ b/nautilus_core/backtest/Cargo.toml @@ -26,6 +26,7 @@ rstest = { workspace = true} cbindgen = { workspace = true, optional = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -47,4 +48,3 @@ python = [ "nautilus-execution/python", "nautilus-model/python", ] -default = ["ffi", "python"] diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index afa48233f39b..5a01a635feff 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -39,6 +39,7 @@ tempfile = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -48,4 +49,3 @@ ffi = ["cbindgen", "nautilus-core/ffi", "nautilus-model/ffi"] python = ["pyo3", "pyo3-asyncio", "nautilus-core/python", "nautilus-model/python"] stubs = ["rstest", "nautilus-model/stubs"] redis = ["dep:redis"] -default = [] diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index 833ff7a6e23f..60fbf23c0641 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -30,8 +30,7 @@ rstest = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = [] extension-module = ["pyo3/extension-module"] ffi = ["cbindgen"] python = ["pyo3"] -default = [] - diff --git a/nautilus_core/execution/Cargo.toml b/nautilus_core/execution/Cargo.toml index 1c2df67aa6a9..e7b55093f758 100644 --- a/nautilus_core/execution/Cargo.toml +++ b/nautilus_core/execution/Cargo.toml @@ -33,6 +33,7 @@ criterion = { workspace = true } rstest = { workspace = true } [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -51,4 +52,3 @@ python = [ "nautilus-core/python", "nautilus-model/python", ] -default = ["ffi", "python"] diff --git a/nautilus_core/indicators/Cargo.toml b/nautilus_core/indicators/Cargo.toml index 9fe555b848ea..2d219672e665 100644 --- a/nautilus_core/indicators/Cargo.toml +++ b/nautilus_core/indicators/Cargo.toml @@ -21,6 +21,7 @@ strum = { workspace = true } rstest = { workspace = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -31,4 +32,3 @@ python = [ "nautilus-core/python", "nautilus-model/python", ] -default = [] diff --git a/nautilus_core/infrastructure/Cargo.toml b/nautilus_core/infrastructure/Cargo.toml index 7a50ffb45532..5a0e69816669 100644 --- a/nautilus_core/infrastructure/Cargo.toml +++ b/nautilus_core/infrastructure/Cargo.toml @@ -25,6 +25,7 @@ tracing = {workspace = true } rstest = { workspace = true } [features] +default = ["redis"] extension-module = [ "pyo3/extension-module", "nautilus-common/extension-module", @@ -33,4 +34,3 @@ extension-module = [ ] python = ["pyo3"] redis = ["dep:redis"] -default = ["redis"] diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index 6819d59bc779..fc6d0609bd75 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -39,6 +39,8 @@ iai = { workspace = true } cbindgen = { workspace = true, optional = true } [features] +default = ["trivial_copy"] +trivial_copy = [] # Enables deriving the `Copy` trait for data types (should be included in default) extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -46,8 +48,6 @@ extension-module = [ ffi = ["cbindgen", "nautilus-core/ffi"] python = ["pyo3", "nautilus-core/python"] stubs = ["rstest"] -trivial_copy = [] # Enables deriving the `Copy` trait for data types (should be included in default) -default = ["trivial_copy"] [[bench]] name = "criterion_fixed_precision_benchmark" diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index 38082c589211..78f574555f68 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -34,9 +34,9 @@ axum = "0.7.4" tracing-test = "0.2.4" [features] +default = ["python"] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", ] python = ["pyo3", "pyo3-asyncio"] -default = ["python"] diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index b5211c278091..b1f7d2abba58 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -34,6 +34,7 @@ quickcheck_macros = "1" procfs = "0.16.0" [features] +default = ["ffi", "python"] extension-module = [ "pyo3/extension-module", "nautilus-core/extension-module", @@ -41,7 +42,6 @@ extension-module = [ ] ffi = ["nautilus-core/ffi", "nautilus-model/ffi"] python = ["pyo3", "nautilus-core/python", "nautilus-model/python"] -default = ["ffi", "python"] [[bench]] name = "bench_persistence" diff --git a/nautilus_core/pyo3/Cargo.toml b/nautilus_core/pyo3/Cargo.toml index 19ddeeb377d4..7be3033864da 100644 --- a/nautilus_core/pyo3/Cargo.toml +++ b/nautilus_core/pyo3/Cargo.toml @@ -23,6 +23,7 @@ nautilus-persistence = { path = "../persistence" , features = ["python"] } pyo3 = { workspace = true } [features] +default = [] extension-module = [ "pyo3/extension-module", "nautilus-accounting/extension-module", @@ -41,4 +42,3 @@ ffi = [ "nautilus-model/ffi", "nautilus-persistence/ffi", ] -default = [] From dbf776b61bff18586939cc055a45833b83e35c77 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 15:05:19 +1100 Subject: [PATCH 31/71] Add CSV loader params Credit @rterbush --- RELEASES.md | 1 + nautilus_trader/persistence/loaders.py | 38 ++++++++++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index ff74f69b10e2..873b3d4bcb2c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Released on TBD (UTC). ### Enhancements - Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection +- Added CSV tick and bar data loaders params, thanks @rterbush - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging - Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton diff --git a/nautilus_trader/persistence/loaders.py b/nautilus_trader/persistence/loaders.py index f8cdb7c0a0a5..8c3c84d64c52 100644 --- a/nautilus_trader/persistence/loaders.py +++ b/nautilus_trader/persistence/loaders.py @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from os import PathLike +from typing import Any import pandas as pd @@ -27,7 +28,9 @@ class CSVTickDataLoader: def load( file_path: PathLike[str] | str, index_col: str | int = "timestamp", - format: str = "mixed", + parse_dates: bool = True, + datetime_format: str = "mixed", + **kwargs: Any, ) -> pd.DataFrame: """ Return a tick `pandas.DataFrame` loaded from the given CSV `file_path`. @@ -36,10 +39,14 @@ def load( ---------- file_path : str, path object or file-like object The path to the CSV file. - index_col : str | int, default 'timestamp' - The index column. - format : str, default 'mixed' + index_col : str or int, default 'timestamp' + The column to use as the row labels of the DataFrame. + parse_dates : bool, default True + If True, attempt to parse the index. + datetime_format : str, default 'mixed' The timestamp column format. + **kwargs : Any + The additional parameters to be passed to pd.read_csv. Returns ------- @@ -49,9 +56,10 @@ def load( df = pd.read_csv( file_path, index_col=index_col, - parse_dates=True, + parse_dates=parse_dates, + **kwargs, ) - df.index = pd.to_datetime(df.index, format=format) + df.index = pd.to_datetime(df.index, format=datetime_format) return df @@ -61,7 +69,12 @@ class CSVBarDataLoader: """ @staticmethod - def load(file_path: PathLike[str] | str) -> pd.DataFrame: + def load( + file_path: PathLike[str] | str, + index_col: str | int = "timestamp", + parse_dates: bool = True, + **kwargs: Any, + ) -> pd.DataFrame: """ Return the bar `pandas.DataFrame` loaded from the given CSV `file_path`. @@ -69,6 +82,12 @@ def load(file_path: PathLike[str] | str) -> pd.DataFrame: ---------- file_path : str, path object or file-like object The path to the CSV file. + index_col : str | int, default 'timestamp' + The column to use as the row labels of the DataFrame. + parse_dates : bool, default True + If True, attempt to parse the index. + **kwargs : Any + The additional parameters to be passed to pd.read_csv. Returns ------- @@ -77,8 +96,9 @@ def load(file_path: PathLike[str] | str) -> pd.DataFrame: """ df = pd.read_csv( file_path, - index_col="timestamp", - parse_dates=True, + index_col=index_col, + parse_dates=parse_dates, + **kwargs, ) df.index = pd.to_datetime(df.index, format="mixed") return df From 4bdf54b1418aefc83845f0863def77d5d4c797f2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 17:04:35 +1100 Subject: [PATCH 32/71] Implement LogGuard for Cython and pyo3 Credit @ayush-sb and @twitu --- nautilus_core/common/src/ffi/logging.rs | 26 +++++++--- nautilus_core/common/src/logging/logger.rs | 52 +++++++++++++++++--- nautilus_core/common/src/logging/mod.rs | 6 +-- nautilus_core/common/src/python/logging.rs | 24 +++++----- nautilus_core/common/src/python/mod.rs | 7 ++- nautilus_trader/common/component.pxd | 8 +++- nautilus_trader/common/component.pyx | 56 +++++++++++++++------- nautilus_trader/core/includes/common.h | 40 +++++++++++----- nautilus_trader/core/nautilus_pyo3.pyi | 10 +++- nautilus_trader/core/rust/common.pxd | 36 +++++++++----- nautilus_trader/system/kernel.py | 6 ++- 11 files changed, 194 insertions(+), 77 deletions(-) diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index 903d48d21235..7531b6f63a9f 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -28,12 +28,20 @@ use crate::{ enums::{LogColor, LogLevel}, logging::{ self, headers, - logger::{self, LoggerConfig}, + logger::{self, LogGuard, LoggerConfig}, logging_set_bypass, map_log_level_to_filter, parse_component_levels, writer::FileWriterConfig, }, }; +/// Wrapper for LogGuard. +/// +/// LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is +/// boxed. +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct LogGuard_API(Box); + /// Initializes logging. /// /// Logging should be used for Python and sync Rust logic which is most of @@ -63,7 +71,7 @@ pub unsafe extern "C" fn logging_init( is_colored: u8, is_bypassed: u8, print_config: u8, -) { +) -> LogGuard_API { let level_stdout = map_log_level_to_filter(level_stdout); let level_file = map_log_level_to_filter(level_file); @@ -87,7 +95,13 @@ pub unsafe extern "C" fn logging_init( logging_set_bypass(); } - logging::init_logging(trader_id, instance_id, config, file_config); + LogGuard_API(Box::new(logging::init_logging( + trader_id, + instance_id, + config, + file_config, + ))) + // logging::init_logging(trader_id, instance_id, config, file_config); } /// Creates a new log event. @@ -138,8 +152,8 @@ pub unsafe extern "C" fn logging_log_sysinfo(component_ptr: *const c_char) { headers::log_sysinfo(component) } -/// Flushes global logger buffers. +/// Flushes global logger buffers of any records. #[no_mangle] -pub extern "C" fn logger_flush() { - log::logger().flush() +pub extern "C" fn logger_drop(log_guard: LogGuard_API) { + drop(log_guard) } diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index b6da47a59207..7d0203a8c98b 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -291,17 +291,23 @@ impl Log for Logger { #[allow(clippy::too_many_arguments)] impl Logger { - pub fn init_with_env(trader_id: TraderId, instance_id: UUID4, file_config: FileWriterConfig) { + #[must_use] + pub fn init_with_env( + trader_id: TraderId, + instance_id: UUID4, + file_config: FileWriterConfig, + ) -> LogGuard { let config = LoggerConfig::from_env(); - Logger::init_with_config(trader_id, instance_id, config, file_config); + Logger::init_with_config(trader_id, instance_id, config, file_config) } + #[must_use] pub fn init_with_config( trader_id: TraderId, instance_id: UUID4, config: LoggerConfig, file_config: FileWriterConfig, - ) { + ) -> LogGuard { let (tx, rx) = channel::(); let logger = Self { @@ -340,6 +346,8 @@ impl Logger { eprintln!("Cannot set logger because of error: {e}") } } + + LogGuard::new() } fn handle_messages( @@ -453,6 +461,32 @@ pub fn log(level: LogLevel, color: LogColor, component: Ustr, message: &str) { } } +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common") +)] +#[repr(C)] +#[derive(Debug)] +pub struct LogGuard {} + +impl LogGuard { + pub fn new() -> Self { + LogGuard {} + } +} + +impl Default for LogGuard { + fn default() -> Self { + Self::new() + } +} + +impl Drop for LogGuard { + fn drop(&mut self) { + log::logger().flush(); + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -539,7 +573,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -582,6 +616,8 @@ mod tests { Duration::from_secs(2), ); + drop(log_guard); // Ensure log buffers are flushed + assert_eq!( log_contents, "1970-01-20T02:20:00.000000000Z [INFO] TRADER-001.RiskEngine: This is a test.\n" @@ -598,7 +634,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -631,6 +667,8 @@ mod tests { Duration::from_secs(3), ); + drop(log_guard); // Ensure log buffers are flushed + assert!( std::fs::read_dir(&temp_dir) .expect("Failed to read directory") @@ -652,7 +690,7 @@ mod tests { ..Default::default() }; - Logger::init_with_config( + let log_guard = Logger::init_with_config( TraderId::from("TRADER-001"), UUID4::new(), config, @@ -687,6 +725,8 @@ mod tests { Duration::from_secs(2), ); + drop(log_guard); // Ensure log buffers are flushed + assert_eq!( log_contents, "{\"timestamp\":\"1970-01-20T02:20:00.000000000Z\",\"trader_id\":\"TRADER-001\",\"level\":\"INFO\",\"color\":\"NORMAL\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs index d7f4b9e5eca6..55d4bf47f710 100644 --- a/nautilus_core/common/src/logging/mod.rs +++ b/nautilus_core/common/src/logging/mod.rs @@ -27,7 +27,7 @@ use tracing_subscriber::EnvFilter; use ustr::Ustr; use self::{ - logger::{Logger, LoggerConfig}, + logger::{LogGuard, Logger, LoggerConfig}, writer::FileWriterConfig, }; use crate::enums::LogLevel; @@ -116,10 +116,10 @@ pub fn init_logging( instance_id: UUID4, config: LoggerConfig, file_config: FileWriterConfig, -) { +) -> LogGuard { LOGGING_INITIALIZED.store(true, Ordering::Relaxed); LOGGING_COLORED.store(config.is_colored, Ordering::Relaxed); - Logger::init_with_config(trader_id, instance_id, config, file_config); + Logger::init_with_config(trader_id, instance_id, config, file_config) } pub fn map_log_level_to_filter(log_level: LogLevel) -> LevelFilter { diff --git a/nautilus_core/common/src/python/logging.rs b/nautilus_core/common/src/python/logging.rs index c6d8d903afa2..a4ad91bf8a43 100644 --- a/nautilus_core/common/src/python/logging.rs +++ b/nautilus_core/common/src/python/logging.rs @@ -25,12 +25,21 @@ use crate::{ enums::{LogColor, LogLevel}, logging::{ self, headers, - logger::{self, LoggerConfig}, + logger::{self, LogGuard, LoggerConfig}, logging_set_bypass, map_log_level_to_filter, parse_level_filter_str, writer::FileWriterConfig, }, }; +#[pymethods] +impl LoggerConfig { + #[staticmethod] + #[pyo3(name = "from_spec")] + pub fn py_from_spec(spec: String) -> Self { + LoggerConfig::from_spec(&spec) + } +} + #[pymethods] impl FileWriterConfig { #[new] @@ -43,15 +52,6 @@ impl FileWriterConfig { } } -#[pymethods] -impl LoggerConfig { - #[staticmethod] - #[pyo3(name = "from_spec")] - pub fn py_from_spec(spec: String) -> Self { - LoggerConfig::from_spec(&spec) - } -} - /// Initialize tracing. /// /// Tracing is meant to be used to trace/debug async Rust code. It can be @@ -94,7 +94,7 @@ pub fn py_init_logging( is_colored: Option, is_bypassed: Option, print_config: Option, -) { +) -> LogGuard { let level_file = level_file .map(map_log_level_to_filter) .unwrap_or(LevelFilter::Off); @@ -113,7 +113,7 @@ pub fn py_init_logging( logging_set_bypass(); } - logging::init_logging(trader_id, instance_id, config, file_config); + logging::init_logging(trader_id, instance_id, config, file_config) } fn parse_component_levels( diff --git a/nautilus_core/common/src/python/mod.rs b/nautilus_core/common/src/python/mod.rs index f96c979e0790..cf5e6c3a54ca 100644 --- a/nautilus_core/common/src/python/mod.rs +++ b/nautilus_core/common/src/python/mod.rs @@ -21,8 +21,6 @@ pub mod versioning; use pyo3::prelude::*; -use crate::logging::{logger::LoggerConfig, writer::FileWriterConfig}; - /// Loaded as nautilus_pyo3.common #[pymodule] pub fn common(_: Python<'_>, m: &PyModule) -> PyResult<()> { @@ -31,8 +29,9 @@ pub fn common(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(logging::py_init_tracing, m)?)?; m.add_function(wrap_pyfunction!(logging::py_init_logging, m)?)?; m.add_function(wrap_pyfunction!(logging::py_logger_log, m)?)?; diff --git a/nautilus_trader/common/component.pxd b/nautilus_trader/common/component.pxd index 09f9678c308d..6c0b57d6f97f 100644 --- a/nautilus_trader/common/component.pxd +++ b/nautilus_trader/common/component.pxd @@ -29,6 +29,7 @@ from nautilus_trader.core.rust.common cimport ComponentState from nautilus_trader.core.rust.common cimport ComponentTrigger from nautilus_trader.core.rust.common cimport LiveClock_API from nautilus_trader.core.rust.common cimport LogColor +from nautilus_trader.core.rust.common cimport LogGuard_API from nautilus_trader.core.rust.common cimport LogLevel from nautilus_trader.core.rust.common cimport MessageBus_API from nautilus_trader.core.rust.common cimport TestClock_API @@ -136,7 +137,12 @@ cpdef str log_color_to_str(LogColor value) cpdef LogLevel log_level_from_str(str value) cpdef str log_level_to_str(LogLevel value) -cpdef void init_logging( + +cdef class LogGuard: + cdef LogGuard_API _mem + + +cpdef LogGuard init_logging( TraderId trader_id=*, str machine_id=*, UUID4 instance_id=*, diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index ee5675c5120c..df3d41254ffa 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -59,6 +59,7 @@ from nautilus_trader.core.message cimport Event from nautilus_trader.core.rust.common cimport ComponentState from nautilus_trader.core.rust.common cimport ComponentTrigger from nautilus_trader.core.rust.common cimport LogColor +from nautilus_trader.core.rust.common cimport LogGuard_API from nautilus_trader.core.rust.common cimport LogLevel from nautilus_trader.core.rust.common cimport TimeEventHandler_t from nautilus_trader.core.rust.common cimport component_state_from_cstr @@ -82,7 +83,7 @@ from nautilus_trader.core.rust.common cimport log_color_from_cstr from nautilus_trader.core.rust.common cimport log_color_to_cstr from nautilus_trader.core.rust.common cimport log_level_from_cstr from nautilus_trader.core.rust.common cimport log_level_to_cstr -from nautilus_trader.core.rust.common cimport logger_flush +from nautilus_trader.core.rust.common cimport logger_drop from nautilus_trader.core.rust.common cimport logger_log from nautilus_trader.core.rust.common cimport logging_clock_set_realtime_mode from nautilus_trader.core.rust.common cimport logging_clock_set_static_mode @@ -1023,7 +1024,19 @@ cpdef str log_level_to_str(LogLevel value): return cstr_to_pystr(log_level_to_cstr(value)) -cpdef void init_logging( +cdef class LogGuard: + """ + Provides a `LogGuard` which serves as a token to signal the initialization + of the logging system. It also ensures that the global logger is flushed + of any buffered records when the instance is destroyed. + """ + + def __del__(self) -> None: + if self._mem._0 != NULL: + logger_drop(self._mem) + + +cpdef LogGuard init_logging( TraderId trader_id = None, str machine_id = None, UUID4 instance_id = None, @@ -1076,6 +1089,10 @@ cpdef void init_logging( print_config : bool, default False If the core logging configuration should be printed to stdout on initialization. + Returns + ------- + LogGuard or ``None`` + """ if trader_id is None: trader_id = TraderId("TRADER-000") @@ -1084,20 +1101,27 @@ cpdef void init_logging( if instance_id is None: instance_id = UUID4() - if not logging_is_initialized(): - logging_init( - trader_id._mem, - instance_id._mem, - level_stdout, - level_file, - pystr_to_cstr(directory) if directory else NULL, - pystr_to_cstr(file_name) if file_name else NULL, - pystr_to_cstr(file_format) if file_format else NULL, - pybytes_to_cstr(msgspec.json.encode(component_levels)) if component_levels else NULL, - colors, - bypass, - print_config, - ) + if logging_is_initialized(): + raise RuntimeError("Logging system already initialized") + + cdef LogGuard_API log_guard_api = logging_init( + trader_id._mem, + instance_id._mem, + level_stdout, + level_file, + pystr_to_cstr(directory) if directory else NULL, + pystr_to_cstr(file_name) if file_name else NULL, + pystr_to_cstr(file_format) if file_format else NULL, + pybytes_to_cstr(msgspec.json.encode(component_levels)) if component_levels else NULL, + colors, + bypass, + print_config, + ) + + cdef LogGuard log_guard = LogGuard.__new__(LogGuard) + log_guard._mem = log_guard_api + return log_guard + LOGGING_PYO3 = False diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index ebaa8c58520d..8d24a083a1b9 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -250,6 +250,20 @@ typedef struct LiveClock_API { struct LiveClock *_0; } LiveClock_API; +typedef struct LogGuard { + +} LogGuard; + +/** + * Wrapper for LogGuard. + * + * LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is + * boxed. + */ +typedef struct LogGuard_API { + struct LogGuard *_0; +} LogGuard_API; + /** * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`MessageBus`]. * @@ -534,17 +548,17 @@ enum LogColor log_color_from_cstr(const char *ptr); * - Assume `file_format_ptr` is either NULL or a valid C string pointer. * - Assume `component_level_ptr` is either NULL or a valid C string pointer. */ -void logging_init(TraderId_t trader_id, - UUID4_t instance_id, - enum LogLevel level_stdout, - enum LogLevel level_file, - const char *directory_ptr, - const char *file_name_ptr, - const char *file_format_ptr, - const char *component_levels_ptr, - uint8_t is_colored, - uint8_t is_bypassed, - uint8_t print_config); +struct LogGuard_API logging_init(TraderId_t trader_id, + UUID4_t instance_id, + enum LogLevel level_stdout, + enum LogLevel level_file, + const char *directory_ptr, + const char *file_name_ptr, + const char *file_format_ptr, + const char *component_levels_ptr, + uint8_t is_colored, + uint8_t is_bypassed, + uint8_t print_config); /** * Creates a new log event. @@ -582,9 +596,9 @@ void logging_log_header(TraderId_t trader_id, void logging_log_sysinfo(const char *component_ptr); /** - * Flushes global logger buffers. + * Flushes global logger buffers of any records. */ -void logger_flush(void); +void logger_drop(struct LogGuard_API log_guard); /** * # Safety diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 45b2fbd6e588..ce67b27fb0b0 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -197,6 +197,14 @@ def convert_to_snake_case(s: str) -> str: ### Logging +class LogGuard: + """ + Provides a `LogGuard` which serves as a token to signal the initialization + of the logging system. It also ensures that the global logger is flushed + of any buffered records when the instance is destroyed. + + """ + def init_tracing() -> None: ... @@ -212,7 +220,7 @@ def init_logging( is_colored: bool | None = None, is_bypassed: bool | None = None, print_config: bool | None = None, -) -> None: ... +) -> LogGuard: ... def log_header( trader_id: TraderId, diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index f207994af6c1..534b940f5806 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -153,6 +153,16 @@ cdef extern from "../includes/common.h": cdef struct LiveClock_API: LiveClock *_0; + cdef struct LogGuard: + pass + + # Wrapper for LogGuard. + # + # LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is + # boxed. + cdef struct LogGuard_API: + LogGuard *_0; + # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`MessageBus`]. # # This struct wraps `MessageBus` in a way that makes it compatible with C function @@ -370,17 +380,17 @@ cdef extern from "../includes/common.h": # - Assume `file_name_ptr` is either NULL or a valid C string pointer. # - Assume `file_format_ptr` is either NULL or a valid C string pointer. # - Assume `component_level_ptr` is either NULL or a valid C string pointer. - void logging_init(TraderId_t trader_id, - UUID4_t instance_id, - LogLevel level_stdout, - LogLevel level_file, - const char *directory_ptr, - const char *file_name_ptr, - const char *file_format_ptr, - const char *component_levels_ptr, - uint8_t is_colored, - uint8_t is_bypassed, - uint8_t print_config); + LogGuard_API logging_init(TraderId_t trader_id, + UUID4_t instance_id, + LogLevel level_stdout, + LogLevel level_file, + const char *directory_ptr, + const char *file_name_ptr, + const char *file_format_ptr, + const char *component_levels_ptr, + uint8_t is_colored, + uint8_t is_bypassed, + uint8_t print_config); # Creates a new log event. # @@ -411,8 +421,8 @@ cdef extern from "../includes/common.h": # - Assumes `component_ptr` is a valid C string pointer. void logging_log_sysinfo(const char *component_ptr); - # Flushes global logger buffers. - void logger_flush(); + # Flushes global logger buffers of any records. + void logger_drop(LogGuard_API log_guard); # # Safety # diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index 56bac70586e7..33d5196fb2ec 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -159,6 +159,7 @@ def __init__( # noqa (too complex) # Setup logging logging: LoggingConfig = config.logging or LoggingConfig() + log_guard = None if not is_logging_initialized(): if not logging.bypass_logging: @@ -168,7 +169,7 @@ def __init__( # noqa (too complex) nautilus_pyo3.init_tracing() # Initialize logging for sync Rust and Python - nautilus_pyo3.init_logging( + log_guard = nautilus_pyo3.init_logging( trader_id=nautilus_pyo3.TraderId(self._trader_id.value), instance_id=nautilus_pyo3.UUID4(self._instance_id.value), level_stdout=nautilus_pyo3.LogLevel(logging.log_level), @@ -187,7 +188,7 @@ def __init__( # noqa (too complex) ) else: # Initialize logging for sync Rust and Python - init_logging( + log_guard = init_logging( trader_id=self._trader_id, machine_id=self._machine_id, instance_id=self._instance_id, @@ -218,6 +219,7 @@ def __init__( # noqa (too complex) ) self._log: Logger = Logger(name=name) + self._log_guard = log_guard self._log.info("Building system kernel...") From 80f897877317e270f09a3783fe6a425e775954a2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 17:13:00 +1100 Subject: [PATCH 33/71] Remove redundant logging initializations --- RELEASES.md | 1 + tests/integration_tests/live/test_live_node.py | 9 --------- tests/performance_tests/test_perf_logger.py | 4 +++- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 873b3d4bcb2c..427c4f8e4cc5 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,6 +5,7 @@ Released on TBD (UTC). ### Enhancements - Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection - Added CSV tick and bar data loaders params, thanks @rterbush +- Implemented `LogGuard` to ensure global logger is flushed on termination, thanks @ayush-sb and @twitu - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging - Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton diff --git a/tests/integration_tests/live/test_live_node.py b/tests/integration_tests/live/test_live_node.py index 4aba914e7c3e..745166b907d5 100644 --- a/tests/integration_tests/live/test_live_node.py +++ b/tests/integration_tests/live/test_live_node.py @@ -23,7 +23,6 @@ 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.common.component import init_logging from nautilus_trader.config import InstrumentProviderConfig from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig @@ -89,10 +88,6 @@ class TestTradingNodeConfiguration: - def setup(self): - # Pre-initialize logging and bypass to avoid the `InvalidConfiguration` exception - init_logging(bypass=True) - def teardown(self): ensure_all_tasks_completed() @@ -211,10 +206,6 @@ def test_setting_instance_id(self, monkeypatch): class TestTradingNodeOperation: - def setup(self): - # Pre-initialize logging and bypass to avoid the `InvalidConfiguration` exception - init_logging(bypass=True) - def teardown(self): ensure_all_tasks_completed() diff --git a/tests/performance_tests/test_perf_logger.py b/tests/performance_tests/test_perf_logger.py index 472cd7890ad5..6712147054e6 100644 --- a/tests/performance_tests/test_perf_logger.py +++ b/tests/performance_tests/test_perf_logger.py @@ -18,12 +18,14 @@ from nautilus_trader.common.component import Logger from nautilus_trader.common.component import init_logging +from nautilus_trader.common.component import is_logging_initialized from nautilus_trader.common.enums import LogLevel def test_logging(benchmark: Any) -> None: random.seed(45362718) - init_logging(level_stdout=LogLevel.ERROR, bypass=True) + if not is_logging_initialized: + init_logging(level_stdout=LogLevel.ERROR, bypass=True) logger = Logger(name="TEST_LOGGER") From 546158edca0960447ecb25dda6a0184f1ae32a68 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 17:22:07 +1100 Subject: [PATCH 34/71] Cleanup LogGuard assignment and init_logging docs --- nautilus_trader/common/component.pyx | 2 +- nautilus_trader/system/kernel.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index df3d41254ffa..97fce74dd98d 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -1091,7 +1091,7 @@ cpdef LogGuard init_logging( Returns ------- - LogGuard or ``None`` + LogGuard """ if trader_id is None: diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index 33d5196fb2ec..7413cac41e4d 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -157,9 +157,8 @@ def __init__( # noqa (too complex) register_component_clock(self._instance_id, self._clock) - # Setup logging + # Initialize logging system logging: LoggingConfig = config.logging or LoggingConfig() - log_guard = None if not is_logging_initialized(): if not logging.bypass_logging: @@ -169,7 +168,7 @@ def __init__( # noqa (too complex) nautilus_pyo3.init_tracing() # Initialize logging for sync Rust and Python - log_guard = nautilus_pyo3.init_logging( + self._log_guard = nautilus_pyo3.init_logging( trader_id=nautilus_pyo3.TraderId(self._trader_id.value), instance_id=nautilus_pyo3.UUID4(self._instance_id.value), level_stdout=nautilus_pyo3.LogLevel(logging.log_level), @@ -188,7 +187,7 @@ def __init__( # noqa (too complex) ) else: # Initialize logging for sync Rust and Python - log_guard = init_logging( + self._log_guard = init_logging( trader_id=self._trader_id, machine_id=self._machine_id, instance_id=self._instance_id, @@ -219,8 +218,6 @@ def __init__( # noqa (too complex) ) self._log: Logger = Logger(name=name) - self._log_guard = log_guard - self._log.info("Building system kernel...") # Setup loop (if sandbox live) From 0b8d40ebd5c0eab349abe44e84e21759c62d1162 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 17:25:20 +1100 Subject: [PATCH 35/71] Complete init_logging docs --- nautilus_trader/common/component.pyx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/common/component.pyx b/nautilus_trader/common/component.pyx index 97fce74dd98d..3023f239ee0c 100644 --- a/nautilus_trader/common/component.pyx +++ b/nautilus_trader/common/component.pyx @@ -1056,7 +1056,8 @@ cpdef LogGuard init_logging( Acts as an interface into the logging system implemented in Rust with the `log` crate. This function should only be called once per process, at the beginning of the application - run. + run. Subsequent calls will raise a `RuntimeError`, as there can only be one `LogGuard` + per initialized system. Parameters ---------- @@ -1093,6 +1094,11 @@ cpdef LogGuard init_logging( ------- LogGuard + Raises + ------ + RuntimeError + If the logging system has already been initialized. + """ if trader_id is None: trader_id = TraderId("TRADER-000") From 0d7bd17625552e1b9a64df9c74ef8bda7d5cd1cf Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 18:01:44 +1100 Subject: [PATCH 36/71] Fix LogGuard deref impl and C declaration --- nautilus_core/common/src/ffi/logging.rs | 29 +++++++++++++++++++--- nautilus_core/common/src/logging/logger.rs | 1 - nautilus_trader/core/includes/common.h | 16 ++++++------ nautilus_trader/core/rust/common.pxd | 16 +++++++----- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index 7531b6f63a9f..14a98ef0d17c 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -13,7 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::ffi::c_char; +use std::{ + ffi::c_char, + ops::{Deref, DerefMut}, +}; use nautilus_core::{ ffi::{ @@ -34,14 +37,32 @@ use crate::{ }, }; -/// Wrapper for LogGuard. +/// Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. +/// +/// This struct wraps `LogGuard` in a way that makes it compatible with C function +/// calls, enabling interaction with `LogGuard` in a C environment. /// -/// LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is -/// boxed. +/// It implements the `Deref` trait, allowing instances of `LogGuard_API` to be +/// dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without +/// having to manually access the underlying `LogGuard` instance.] #[repr(C)] #[allow(non_camel_case_types)] pub struct LogGuard_API(Box); +impl Deref for LogGuard_API { + type Target = LogGuard; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for LogGuard_API { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + /// Initializes logging. /// /// Logging should be used for Python and sync Rust logic which is most of diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 7d0203a8c98b..e7ac3d32211d 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -465,7 +465,6 @@ pub fn log(level: LogLevel, color: LogColor, component: Ustr, message: &str) { feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.common") )] -#[repr(C)] #[derive(Debug)] pub struct LogGuard {} diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index 8d24a083a1b9..e1230f95325b 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -195,6 +195,8 @@ typedef enum LogLevel { typedef struct LiveClock LiveClock; +typedef struct LogGuard LogGuard; + /** * Provides a generic message bus to facilitate various messaging patterns. * @@ -250,15 +252,15 @@ typedef struct LiveClock_API { struct LiveClock *_0; } LiveClock_API; -typedef struct LogGuard { - -} LogGuard; - /** - * Wrapper for LogGuard. + * Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + * + * This struct wraps `LogGuard` in a way that makes it compatible with C function + * calls, enabling interaction with `LogGuard` in a C environment. * - * LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is - * boxed. + * It implements the `Deref` trait, allowing instances of `LogGuard_API` to be + * dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without + * having to manually access the underlying `LogGuard` instance.] */ typedef struct LogGuard_API { struct LogGuard *_0; diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 534b940f5806..053994e21e44 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -104,6 +104,9 @@ cdef extern from "../includes/common.h": cdef struct LiveClock: pass + cdef struct LogGuard: + pass + # Provides a generic message bus to facilitate various messaging patterns. # # The bus provides both a producer and consumer API for Pub/Sub, Req/Rep, as @@ -153,13 +156,14 @@ cdef extern from "../includes/common.h": cdef struct LiveClock_API: LiveClock *_0; - cdef struct LogGuard: - pass - - # Wrapper for LogGuard. + # Provides a C compatible Foreign Function Interface (FFI) for an underlying [`LogGuard`]. + # + # This struct wraps `LogGuard` in a way that makes it compatible with C function + # calls, enabling interaction with `LogGuard` in a C environment. # - # LogGuard is an empty struct, which is not FFI-safe. To avoid errors, it is - # boxed. + # It implements the `Deref` trait, allowing instances of `LogGuard_API` to be + # dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without + # having to manually access the underlying `LogGuard` instance.] cdef struct LogGuard_API: LogGuard *_0; From 36467baf47c7b18bb636f99d34ceadc847a37f4e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 18:07:15 +1100 Subject: [PATCH 37/71] Remove errant square bracket in docs --- nautilus_core/common/src/ffi/logging.rs | 2 +- nautilus_trader/core/includes/common.h | 2 +- nautilus_trader/core/rust/common.pxd | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nautilus_core/common/src/ffi/logging.rs b/nautilus_core/common/src/ffi/logging.rs index 14a98ef0d17c..8600e0e19514 100644 --- a/nautilus_core/common/src/ffi/logging.rs +++ b/nautilus_core/common/src/ffi/logging.rs @@ -44,7 +44,7 @@ use crate::{ /// /// It implements the `Deref` trait, allowing instances of `LogGuard_API` to be /// dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without -/// having to manually access the underlying `LogGuard` instance.] +/// having to manually access the underlying `LogGuard` instance. #[repr(C)] #[allow(non_camel_case_types)] pub struct LogGuard_API(Box); diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index e1230f95325b..b10ca55c5cab 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -260,7 +260,7 @@ typedef struct LiveClock_API { * * It implements the `Deref` trait, allowing instances of `LogGuard_API` to be * dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without - * having to manually access the underlying `LogGuard` instance.] + * having to manually access the underlying `LogGuard` instance. */ typedef struct LogGuard_API { struct LogGuard *_0; diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 053994e21e44..a49b134a9878 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -163,7 +163,7 @@ cdef extern from "../includes/common.h": # # It implements the `Deref` trait, allowing instances of `LogGuard_API` to be # dereferenced to `LogGuard`, providing access to `LogGuard`'s methods without - # having to manually access the underlying `LogGuard` instance.] + # having to manually access the underlying `LogGuard` instance. cdef struct LogGuard_API: LogGuard *_0; From 53bccda1cbbc2a43ac0f729d2de65cc3fe83371d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 18:24:20 +1100 Subject: [PATCH 38/71] Update dependencies --- nautilus_core/Cargo.lock | 8 ++-- nautilus_core/Cargo.toml | 2 +- poetry.lock | 82 ++++++++++++++++++++-------------------- pyproject.toml | 4 +- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index c7c8a469f8ef..3f4b1563eedf 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -362,9 +362,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" dependencies = [ "proc-macro2", "quote", @@ -572,9 +572,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index cd53abd0bb4e..1ae2e806e592 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -40,7 +40,7 @@ rmp-serde = "1.1.2" rust_decimal = "1.34.3" rust_decimal_macros = "1.34.2" serde = { version = "1.0.197", features = ["derive"] } -serde_json = "1.0.113" +serde_json = "1.0.114" strum = { version = "0.26.2", features = ["derive"] } thiserror = "1.0.58" thousands = "0.2.0" diff --git a/poetry.lock b/poetry.lock index 6cfa48fb7761..13800f280a20 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1487,13 +1487,13 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.0.240218" +version = "2.2.1.240316" description = "Type annotations for pandas" optional = false python-versions = ">=3.9" files = [ - {file = "pandas_stubs-2.2.0.240218-py3-none-any.whl", hash = "sha256:e97478320add9b958391b15a56c5f1bf29da656d5b747d28bbe708454b3a1fe6"}, - {file = "pandas_stubs-2.2.0.240218.tar.gz", hash = "sha256:63138c12eec715d66d48611bdd922f31cd7c78bcadd19384c3bd61fd3720a11a"}, + {file = "pandas_stubs-2.2.1.240316-py3-none-any.whl", hash = "sha256:0126a26451a37cb893ea62357ca87ba3d181bd999ec8ba2ca5602e20207d6682"}, + {file = "pandas_stubs-2.2.1.240316.tar.gz", hash = "sha256:236a4f812fb6b1922e9607ff09e427f6d8540c421c9e5a40e3e4ddf7adac7f05"}, ] [package.dependencies] @@ -1600,47 +1600,47 @@ files = [ [[package]] name = "pyarrow" -version = "15.0.0" +version = "15.0.1" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:0a524532fd6dd482edaa563b686d754c70417c2f72742a8c990b322d4c03a15d"}, - {file = "pyarrow-15.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a6bdb314affa9c2e0d5dddf3d9cbb9ef4a8dddaa68669975287d47ece67642"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66958fd1771a4d4b754cd385835e66a3ef6b12611e001d4e5edfcef5f30391e2"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f500956a49aadd907eaa21d4fff75f73954605eaa41f61cb94fb008cf2e00c6"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6f87d9c4f09e049c2cade559643424da84c43a35068f2a1c4653dc5b1408a929"}, - {file = "pyarrow-15.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85239b9f93278e130d86c0e6bb455dcb66fc3fd891398b9d45ace8799a871a1e"}, - {file = "pyarrow-15.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b8d43e31ca16aa6e12402fcb1e14352d0d809de70edd185c7650fe80e0769e3"}, - {file = "pyarrow-15.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:fa7cd198280dbd0c988df525e50e35b5d16873e2cdae2aaaa6363cdb64e3eec5"}, - {file = "pyarrow-15.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8780b1a29d3c8b21ba6b191305a2a607de2e30dab399776ff0aa09131e266340"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0ec198ccc680f6c92723fadcb97b74f07c45ff3fdec9dd765deb04955ccf19"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036a7209c235588c2f07477fe75c07e6caced9b7b61bb897c8d4e52c4b5f9555"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2bd8a0e5296797faf9a3294e9fa2dc67aa7f10ae2207920dbebb785c77e9dbe5"}, - {file = "pyarrow-15.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e8ebed6053dbe76883a822d4e8da36860f479d55a762bd9e70d8494aed87113e"}, - {file = "pyarrow-15.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d53a9d1b2b5bd7d5e4cd84d018e2a45bc9baaa68f7e6e3ebed45649900ba99"}, - {file = "pyarrow-15.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9950a9c9df24090d3d558b43b97753b8f5867fb8e521f29876aa021c52fda351"}, - {file = "pyarrow-15.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:003d680b5e422d0204e7287bb3fa775b332b3fce2996aa69e9adea23f5c8f970"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f75fce89dad10c95f4bf590b765e3ae98bcc5ba9f6ce75adb828a334e26a3d40"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca9cb0039923bec49b4fe23803807e4ef39576a2bec59c32b11296464623dc2"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ed5a78ed29d171d0acc26a305a4b7f83c122d54ff5270810ac23c75813585e4"}, - {file = "pyarrow-15.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6eda9e117f0402dfcd3cd6ec9bfee89ac5071c48fc83a84f3075b60efa96747f"}, - {file = "pyarrow-15.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a3a6180c0e8f2727e6f1b1c87c72d3254cac909e609f35f22532e4115461177"}, - {file = "pyarrow-15.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:19a8918045993349b207de72d4576af0191beef03ea655d8bdb13762f0cd6eac"}, - {file = "pyarrow-15.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0ec076b32bacb6666e8813a22e6e5a7ef1314c8069d4ff345efa6246bc38593"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5db1769e5d0a77eb92344c7382d6543bea1164cca3704f84aa44e26c67e320fb"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2617e3bf9df2a00020dd1c1c6dce5cc343d979efe10bc401c0632b0eef6ef5b"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:d31c1d45060180131caf10f0f698e3a782db333a422038bf7fe01dace18b3a31"}, - {file = "pyarrow-15.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:c8c287d1d479de8269398b34282e206844abb3208224dbdd7166d580804674b7"}, - {file = "pyarrow-15.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:07eb7f07dc9ecbb8dace0f58f009d3a29ee58682fcdc91337dfeb51ea618a75b"}, - {file = "pyarrow-15.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:47af7036f64fce990bb8a5948c04722e4e3ea3e13b1007ef52dfe0aa8f23cf7f"}, - {file = "pyarrow-15.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93768ccfff85cf044c418bfeeafce9a8bb0cee091bd8fd19011aff91e58de540"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6ee87fd6892700960d90abb7b17a72a5abb3b64ee0fe8db6c782bcc2d0dc0b4"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001fca027738c5f6be0b7a3159cc7ba16a5c52486db18160909a0831b063c4e4"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:d1c48648f64aec09accf44140dccb92f4f94394b8d79976c426a5b79b11d4fa7"}, - {file = "pyarrow-15.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:972a0141be402bb18e3201448c8ae62958c9c7923dfaa3b3d4530c835ac81aed"}, - {file = "pyarrow-15.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f01fc5cf49081426429127aa2d427d9d98e1cb94a32cb961d583a70b7c4504e6"}, - {file = "pyarrow-15.0.0.tar.gz", hash = "sha256:876858f549d540898f927eba4ef77cd549ad8d24baa3207cf1b72e5788b50e83"}, + {file = "pyarrow-15.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c2ddb3be5ea938c329a84171694fc230b241ce1b6b0ff1a0280509af51c375fa"}, + {file = "pyarrow-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7543ea88a0ff72f8e6baaf9bfdbec2c62aeabdbede9e4a571c71cc3bc43b6302"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1519e218a6941fc074e4501088d891afcb2adf77c236e03c34babcf3d6a0d1c7"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cafa86e1944761970d3b3fc0411b14ff9b5c2b73cd22aaf470d7a3976335f5"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be5c3d463e33d03eab496e1af7916b1d44001c08f0f458ad27dc16093a020638"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:47b1eda15d3aa3f49a07b1808648e1397e5dc6a80a30bf87faa8e2d02dad7ac3"}, + {file = "pyarrow-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e524a31be7db22deebbbcf242b189063ab9a7652c62471d296b31bc6e3cae77b"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a476fefe8bdd56122fb0d4881b785413e025858803cc1302d0d788d3522b374d"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:309e6191be385f2e220586bfdb643f9bb21d7e1bc6dd0a6963dc538e347b2431"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83bc586903dbeb4365cbc72b602f99f70b96c5882e5dfac5278813c7d624ca3c"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e652daac6d8b05280cd2af31c0fb61a4490ec6a53dc01588014d9fa3fdbee9"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:abad2e08652df153a72177ce20c897d083b0c4ebeec051239e2654ddf4d3c996"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cde663352bc83ad75ba7b3206e049ca1a69809223942362a8649e37bd22f9e3b"}, + {file = "pyarrow-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1b6e237dd7a08482a8b8f3f6512d258d2460f182931832a8c6ef3953203d31e1"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd167536ee23192760b8c731d39b7cfd37914c27fd4582335ffd08450ff799d"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c08bb31eb2984ba5c3747d375bb522e7e536b8b25b149c9cb5e1c49b0ccb736"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f9c1d630ed2524bd1ddf28ec92780a7b599fd54704cd653519f7ff5aec177a"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5186048493395220550bca7b524420471aac2d77af831f584ce132680f55c3df"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31dc30c7ec8958da3a3d9f31d6c3630429b2091ede0ecd0d989fd6bec129f0e4"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3f111a014fb8ac2297b43a74bf4495cc479a332908f7ee49cb7cbd50714cb0c1"}, + {file = "pyarrow-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a6d1f7c15d7f68f08490d0cb34611497c74285b8a6bbeab4ef3fc20117310983"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9ad931b996f51c2f978ed517b55cb3c6078272fb4ec579e3da5a8c14873b698d"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:738f6b53ab1c2f66b2bde8a1d77e186aeaab702d849e0dfa1158c9e2c030add3"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c1c3fc16bc74e33bf8f1e5a212938ed8d88e902f372c4dac6b5bad328567d2f"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fa92512128f6c1b8dde0468c1454dd70f3bff623970e370d52efd4d24fd0be"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b4157f307c202cbbdac147d9b07447a281fa8e63494f7fc85081da351ec6ace9"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b75e7da26f383787f80ad76143b44844ffa28648fcc7099a83df1538c078d2f2"}, + {file = "pyarrow-15.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a99eac76ae14096c209850935057b9e8ce97a78397c5cde8724674774f34e5d"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:dd532d3177e031e9b2d2df19fd003d0cc0520d1747659fcabbd4d9bb87de508c"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce8c89848fd37e5313fc2ce601483038ee5566db96ba0808d5883b2e2e55dc53"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862eac5e5f3b6477f7a92b2f27e560e1f4e5e9edfca9ea9da8a7478bb4abd5ce"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0ea3a29cd5cb99bf14c1c4533eceaa00ea8fb580950fb5a89a5c771a994a4e"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bb902f780cfd624b2e8fd8501fadab17618fdb548532620ef3d91312aaf0888a"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4f87757f02735a6bb4ad2e1b98279ac45d53b748d5baf52401516413007c6999"}, + {file = "pyarrow-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:efd3816c7fbfcbd406ac0f69873cebb052effd7cdc153ae5836d1b00845845d7"}, + {file = "pyarrow-15.0.1.tar.gz", hash = "sha256:21d812548d39d490e0c6928a7c663f37b96bf764034123d4b4ab4530ecc757a9"}, ] [package.dependencies] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "d43b785d4f415dbe6d3f380828d8449af8cd81c8a88535d83a98d03576048c80" +content-hash = "9c4de1ebaa97361596d80ae81d9bcbd672f01914bbb8962d970fa72ad7aa8d2a" diff --git a/pyproject.toml b/pyproject.toml index c1bd1594fba1..789329e855dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ click = "^8.1.7" fsspec = "==2023.6.0" # Pinned due breaking changes msgspec = "^0.18.6" pandas = "^2.2.1" -pyarrow = "==15.0.0" # 15.0.1 wheels not available for glibc 2.25 yet +pyarrow = ">=15.0.1" pytz = ">=2023.4.0" tqdm = "^4.66.2" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} @@ -81,7 +81,7 @@ optional = true black = "^24.3.0" docformatter = "^1.7.5" mypy = "^1.9.0" -pandas-stubs = "^2.1.4" +pandas-stubs = "^2.2.1" pre-commit = "^3.6.2" ruff = "^0.3.3" types-pytz = "^2023.3" From 3937c145775264eab441cf6ec7bd7ed2802f7841 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 18:31:19 +1100 Subject: [PATCH 39/71] Implement Rust external strategy IDs --- .../model/src/identifiers/strategy_id.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index dff74ac50bc2..ee218db4503d 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -18,6 +18,9 @@ use std::fmt::{Debug, Display, Formatter}; use nautilus_core::correctness::{check_string_contains, check_valid_string}; use ustr::Ustr; +/// The identifier for all 'external' strategy IDs (not local to this system instance). +const EXTERNAL_STRATEGY_ID: &str = "EXTERNAL"; + /// Represents a valid strategy ID. /// /// Must be correctly formatted with two valid strings either side of a hyphen. @@ -42,7 +45,7 @@ pub struct StrategyId { impl StrategyId { pub fn new(value: &str) -> anyhow::Result { check_valid_string(value, stringify!(value))?; - if value != "EXTERNAL" { + if value != EXTERNAL_STRATEGY_ID { check_string_contains(value, "-", stringify!(value))?; } @@ -51,6 +54,18 @@ impl StrategyId { }) } + #[must_use] + pub fn external() -> Self { + Self { + value: Ustr::from(EXTERNAL_STRATEGY_ID), + } + } + + #[must_use] + pub fn is_external(&self) -> bool { + self.value == EXTERNAL_STRATEGY_ID + } + #[must_use] pub fn get_tag(&self) -> &str { // SAFETY: Unwrap safe as value previously validated @@ -100,6 +115,16 @@ mod tests { assert_eq!(format!("{strategy_id_ema_cross}"), "EMACross-001"); } + #[rstest] + fn test_get_external() { + assert_eq!(StrategyId::external().value, "EXTERNAL"); + } + + #[rstest] + fn test_is_external() { + assert!(StrategyId::external().is_external()); + } + #[rstest] fn test_get_tag(strategy_id_ema_cross: StrategyId) { assert_eq!(strategy_id_ema_cross.get_tag(), "001"); From f24afd61af109ff2ee153f6e3e46a8f8def24f18 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 18:49:37 +1100 Subject: [PATCH 40/71] Update docs --- docs/concepts/data.md | 2 +- docs/integrations/databento.md | 56 ++++++++++--------- .../notebooks/databento_data_catalog.ipynb | 2 +- .../adapters/src/databento/loader.rs | 2 +- nautilus_trader/adapters/databento/loaders.py | 2 +- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/docs/concepts/data.md b/docs/concepts/data.md index bbb85470848a..9964a606c860 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -41,7 +41,7 @@ To achieve this, two main components are necessary: ### Data loaders Data loader components are typically specific for the raw source/format and per integration. For instance, Binance order book data is stored in its raw CSV file form with -an entirely different format to [Databento Binary Encoding (DBN)](https://docs.databento.com/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn) files. +an entirely different format to [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn) files. ### Data wranglers diff --git a/docs/integrations/databento.md b/docs/integrations/databento.md index a43710e16e73..ad10aa049e5b 100644 --- a/docs/integrations/databento.md +++ b/docs/integrations/databento.md @@ -1,10 +1,10 @@ # Databento ```{warning} -We are currently working on this integration guide - consider it incomplete for now. +We are currently working on this integration guide. ``` -NautilusTrader provides an adapter for integrating with the Databento API and [Databento Binary Encoding (DBN)](https://docs.databento.com/knowledge-base/new-users/dbn-encoding) format data. +NautilusTrader provides an adapter for integrating with the Databento API and [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding) format data. As Databento is purely a market data provider, there is no execution client provided - although a sandbox environment with simulated execution could still be set up. It's also possible to match Databento data with Interactive Brokers execution, or to calculate traditional asset class signals for crypto trading. @@ -17,7 +17,7 @@ The capabilities of this adapter include: [Databento](https://databento.com/signup) currently offers 125 USD in free data credits (historical data only) for new account sign-ups. With careful requests, this is more than enough for testing and evaluation purposes. -It's recommended you make use of the [/metadata.get_cost](https://docs.databento.com/api-reference-historical/metadata/metadata-get-cost) endpoint. +It's recommended you make use of the [/metadata.get_cost](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost) endpoint. ``` ## Overview @@ -44,13 +44,13 @@ and won't need to necessarily work with these lower level components directly. ## Databento documentation -Databento provides extensive documentation for users which can be found in the [Databento knowledge base](https://docs.databento.com/knowledge-base/new-users). -It's recommended you also refer to the Databento documentation in conjunction with this Nautilus integration guide. +Databento provides extensive documentation for users which can be found in the [Databento knowledge base](https://databento.com/docs/knowledge-base/new-users). +It's recommended you also refer to this Databento documentation in conjunction with this NautilusTrader integration guide. ## Databento Binary Encoding (DBN) Databento Binary Encoding (DBN) is an extremely fast message encoding and storage format for normalized market data. -The [DBN specification](https://docs.databento.com/knowledge-base/new-users/dbn-encoding) includes a simple, self-describing metadata header and a fixed set of struct definitions, +The [DBN specification](https://databento.com/docs/knowledge-base/new-users/dbn-encoding) includes a simple, self-describing metadata header and a fixed set of struct definitions, which enforce a standardized way to normalize market data. The integration provides a decoder which can convert DBN format data to Nautilus objects. @@ -91,7 +91,7 @@ The Nautilus decoder will use the Databento `raw_symbol` for the Nautilus `symbo from the Databento instrument definition message for the Nautilus `venue`. Databento datasets are identified with a *dataset code* which is not the same -as a venue identifier. You can read more about Databento dataset naming conventions [here](https://docs.databento.com/api-reference-historical/basics/datasets). +as a venue identifier. You can read more about Databento dataset naming conventions [here](https://databento.com/docs/api-reference-historical/basics/datasets). Of particular note is for CME Globex MDP 3.0 data (`GLBX.MDP3` dataset code), the following exchanges are all grouped under the `GLBX` venue. These mappings can be determined from the @@ -105,7 +105,7 @@ instruments `exchange` field: - `XNYM` - **New York Mercantile Exchange (NYMEX)** ```{note} -Other venue MICs can be found in the `venue` field of responses from the [metadata.list_publishers](https://docs.databento.com/api-reference-historical/metadata/metadata-list-publishers?historical=http&live=python) endpoint. +Other venue MICs can be found in the `venue` field of responses from the [metadata.list_publishers](https://databento.com/docs/api-reference-historical/metadata/metadata-list-publishers?historical=http&live=python) endpoint. ``` ## Timestamps @@ -127,8 +127,8 @@ When decoding and normalizing Databento to Nautilus we generally assign the Data ```{note} See the following Databento docs for further information: -- [Databento standards and conventions - timestamps](https://docs.databento.com/knowledge-base/new-users/standards-conventions/timestamps) -- [Databento timestamping guide](https://docs.databento.com/knowledge-base/data-integrity/timestamping/timestamps-on-databento-and-how-to-use-them) +- [Databento standards and conventions - timestamps](https://databento.com/docs/knowledge-base/new-users/standards-conventions/timestamps) +- [Databento timestamping guide](https://databento.com/docs/knowledge-base/data-integrity/timestamping/timestamps-on-databento-and-how-to-use-them) ``` ## Data types @@ -136,6 +136,10 @@ See the following Databento docs for further information: The following section discusses Databento schema -> Nautilus data type equivalence and considerations. +```{note} +See the Databento [list of fields by schema guide](https://databento.com/docs/knowledge-base/new-users/fields-by-schema). +``` + ### Instrument definitions Databento provides a single schema to cover all instrument classes, these are @@ -143,17 +147,17 @@ decoded to the appropriate Nautilus `Instrument` types. The following Databento instrument classes are supported by NautilusTrader: -| Databento instrument class | Nautilus instrument type | -|----------------------------|------------------------------| -| STOCK | `Equity` | -| FUTURE | `FuturesContract` | -| CALL | `OptionsContract` | -| PUT | `OptionsContract` | -| FUTURESPREAD | `FuturesSpread` | -| OPTIONSPREAD | `OptionsSpread` | -| MIXEDSPREAD | `OptionsSpread` | -| FXSPOT | `CurrencyPair` | -| BOND | Not yet available | +| Databento instrument class | Code | Nautilus instrument type | +|----------------------------|------|------------------------------| +| Stock | `K` | `Equity` | +| Future | `F` | `FuturesContract` | +| Call | `C` | `OptionsContract` | +| Put | `P` | `OptionsContract` | +| Future spread | `S` | `FuturesSpread` | +| Option spread | `T` | `OptionsSpread` | +| Mixed spread | `M` | `OptionsSpread` | +| FX spot | `X` | `CurrencyPair` | +| Bond | `B` | Not yet available | ### MBO (market by order) @@ -171,9 +175,9 @@ object, which occurs during the replay startup sequence. ### MBP-1 (market by price, top-of-book) -This schema represents the top-of-book only. Like with MBO messages, some +This schema represents the top-of-book only (quotes *and* trades). Like with MBO messages, some messages carry trade information, and so when decoding MBP-1 messages Nautilus -will produce a `QuoteTick` and also a `TradeTick` if the message is a trade. +will produce a `QuoteTick` and *also* a `TradeTick` if the message is a trade. ### OHLCV (bar aggregates) @@ -183,9 +187,9 @@ The Nautilus decoder will normalize the `ts_event` timestamps to the **close** o ### Imbalance & Statistics -The Databento `imbalance` and `statistics` schemas cannot be represented as a built-in Nautilus data types +The Databento `imbalance` and `statistics` schemas cannot be represented as a built-in Nautilus data types, and so they have specific types defined in Rust `DatabentoImbalance` and `DatabentoStatistics`. -Python bindings are provided via pyo3 (Rust) and so the types behaves a little differently to a built-in Nautilus +Python bindings are provided via pyo3 (Rust) so the types behave a little differently to a built-in Nautilus data types, where all attributes are pyo3 provided objects and not directly compatible with certain methods which may expect a Cython provided type. There are pyo3 -> legacy Cython object conversion methods available, which can be found in the API reference. @@ -244,7 +248,7 @@ the Nautilus Parquet data from disk, which achieves extremely high through-put ( than converting DBN -> Nautilus on the fly for every backtest run). ```{note} -Performance benchmarks are under development. +Performance benchmarks are currently under development. ``` ## Loading DBN data diff --git a/examples/notebooks/databento_data_catalog.ipynb b/examples/notebooks/databento_data_catalog.ipynb index a11079005e7c..2cb7aa47dd17 100644 --- a/examples/notebooks/databento_data_catalog.ipynb +++ b/examples/notebooks/databento_data_catalog.ipynb @@ -76,7 +76,7 @@ "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", + "We can use a metadata [get_cost endpoint](https://databento.com/docs/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." diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 635ddcd3e7ed..09dc075dd728 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -55,7 +55,7 @@ use super::{ /// - STATISTICS -> `DatabentoStatistics` /// /// # References -/// +/// #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.databento") diff --git a/nautilus_trader/adapters/databento/loaders.py b/nautilus_trader/adapters/databento/loaders.py index cff1127e5039..91b6e54dece8 100644 --- a/nautilus_trader/adapters/databento/loaders.py +++ b/nautilus_trader/adapters/databento/loaders.py @@ -47,7 +47,7 @@ class DatabentoDataLoader: References ---------- - https://docs.databento.com/knowledge-base/new-users/dbn-encoding + https://databento.com/docs/knowledge-base/new-users/dbn-encoding """ From ff402108b9fc724273e87862cc06e7658c7fd3e2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 19:01:10 +1100 Subject: [PATCH 41/71] Refine Databento adapter --- nautilus_core/Cargo.lock | 3 - nautilus_core/adapters/Cargo.toml | 3 +- .../adapters/src/databento/bin/sandbox.rs | 2 +- .../adapters/src/databento/decode.rs | 1 + nautilus_core/adapters/src/databento/live.rs | 1 + .../adapters/src/databento/loader.rs | 1 + .../adapters/src/databento/python/decode.rs | 126 ------------------ .../src/databento/python/historical.rs | 2 +- .../adapters/src/databento/python/live.rs | 2 +- .../adapters/src/databento/python/loader.rs | 1 + .../adapters/src/databento/python/mod.rs | 9 -- .../adapters/src/databento/symbology.rs | 1 + nautilus_core/adapters/src/databento/types.rs | 1 + 13 files changed, 10 insertions(+), 143 deletions(-) delete mode 100644 nautilus_core/adapters/src/databento/python/decode.rs diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 3f4b1563eedf..8584332e521d 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1344,10 +1344,8 @@ dependencies = [ "itoa", "json-writer", "num_enum", - "pyo3", "serde", "streaming-iterator", - "strum 0.26.2", "thiserror", "time", "tokio", @@ -2438,7 +2436,6 @@ dependencies = [ "chrono", "criterion", "databento", - "dbn", "indexmap 2.2.5", "itoa", "log", diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 92afa3857f9e..18c612ea7216 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -36,7 +36,6 @@ tokio = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } databento = { version = "0.7.1", optional = true } -dbn = { version = "0.16.0", optional = true, features = ["python"] } streaming-iterator = "0.1.9" time = "0.3.34" @@ -52,7 +51,7 @@ extension-module = [ "nautilus-core/extension-module", "nautilus-model/extension-module", ] -databento = ["dep:databento", "dbn", "python"] +databento = ["dep:databento", "python"] ffi = [ "nautilus-common/ffi", "nautilus-core/ffi", diff --git a/nautilus_core/adapters/src/databento/bin/sandbox.rs b/nautilus_core/adapters/src/databento/bin/sandbox.rs index b70f7f96b1f6..27e9ec85151b 100644 --- a/nautilus_core/adapters/src/databento/bin/sandbox.rs +++ b/nautilus_core/adapters/src/databento/bin/sandbox.rs @@ -1,11 +1,11 @@ use std::env; +use databento::dbn::{MboMsg, TradeMsg}; use databento::{ dbn::{Dataset::GlbxMdp3, SType, Schema}, live::Subscription, LiveClient, }; -use dbn::{MboMsg, TradeMsg}; use time::OffsetDateTime; #[tokio::main] diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index c270139d2fa2..aee32b7ffde0 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -20,6 +20,7 @@ use std::{ str::FromStr, }; +use databento::dbn; use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, time::UnixNanos}; use nautilus_model::{ data::{ diff --git a/nautilus_core/adapters/src/databento/live.rs b/nautilus_core/adapters/src/databento/live.rs index d0edc6559a7a..e71ea408fa58 100644 --- a/nautilus_core/adapters/src/databento/live.rs +++ b/nautilus_core/adapters/src/databento/live.rs @@ -16,6 +16,7 @@ use std::{collections::HashMap, ffi::CStr}; use databento::{ + dbn, dbn::{PitSymbolMap, Record, SymbolIndex, VersionUpgradePolicy}, live::Subscription, }; diff --git a/nautilus_core/adapters/src/databento/loader.rs b/nautilus_core/adapters/src/databento/loader.rs index 09dc075dd728..4f171b0cc8e1 100644 --- a/nautilus_core/adapters/src/databento/loader.rs +++ b/nautilus_core/adapters/src/databento/loader.rs @@ -15,6 +15,7 @@ use std::{env, fs, path::PathBuf}; +use databento::dbn; use dbn::{ compat::InstrumentDefMsgV1, decode::{dbn::Decoder, DbnMetadata, DecodeStream}, diff --git a/nautilus_core/adapters/src/databento/python/decode.rs b/nautilus_core/adapters/src/databento/python/decode.rs deleted file mode 100644 index 0abbbfc6a506..000000000000 --- a/nautilus_core/adapters/src/databento/python/decode.rs +++ /dev/null @@ -1,126 +0,0 @@ -// ------------------------------------------------------------------------------------------------- -// 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::time::UnixNanos; -use nautilus_model::{ - data::{depth::OrderBookDepth10, trade::TradeTick}, - identifiers::instrument_id::InstrumentId, - instruments::{ - equity::Equity, futures_contract::FuturesContract, options_contract::OptionsContract, - }, -}; -use pyo3::{prelude::*, types::PyTuple}; - -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 = "decode_equity")] -pub fn py_decode_equity( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_equity_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_futures_contract")] -pub fn py_decode_futures_contract( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_futures_contract_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[pyo3(name = "decode_options_contract")] -pub fn py_decode_options_contract( - record: &dbn::compat::InstrumentDefMsgV1, - instrument_id: InstrumentId, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_options_contract_v1(record, instrument_id, ts_init) -} - -#[pyfunction] -#[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, -) -> anyhow::Result { - let (data, _) = decode_mbo_msg(record, instrument_id, price_precision, ts_init, false)?; - if let Some(data) = data { - Ok(data.into_py(py)) - } else { - anyhow::bail!("Error decoding MBO message") - } -} - -#[pyfunction] -#[pyo3(name = "decode_trade_msg")] -pub fn py_decode_trade_msg( - record: &dbn::TradeMsg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_trade_msg(record, instrument_id, price_precision, ts_init) -} - -#[pyfunction] -#[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, -) -> anyhow::Result { - let (quote, maybe_trade) = decode_mbp1_msg( - record, - instrument_id, - price_precision, - ts_init, - include_trades, - )?; - - let quote_py = quote.into_py(py); - match maybe_trade { - Some(trade) => { - let trade_py = trade.into_py(py); - Ok(PyTuple::new(py, &[quote_py, trade_py]).into_py(py)) - } - None => Ok(PyTuple::new(py, &[quote_py, py.None()]).into_py(py)), - } -} - -#[pyfunction] -#[pyo3(name = "decode_mbp10_msg")] -pub fn py_decode_mbp10_msg( - record: &dbn::Mbp10Msg, - instrument_id: InstrumentId, - price_precision: u8, - ts_init: UnixNanos, -) -> anyhow::Result { - decode_mbp10_msg(record, instrument_id, price_precision, ts_init) -} diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index 9ff0317509ca..aaec96b90685 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -15,7 +15,7 @@ use std::{fs, num::NonZeroU64, sync::Arc}; -use databento::historical::timeseries::GetRangeParams; +use databento::{dbn, historical::timeseries::GetRangeParams}; use indexmap::IndexMap; use nautilus_core::{ python::to_pyvalue_err, diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index 0473d91eb080..e37e6158dfa2 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -15,7 +15,7 @@ use std::{fs, str::FromStr}; -use databento::live::Subscription; +use databento::{dbn, live::Subscription}; use indexmap::IndexMap; use nautilus_core::{ python::{to_pyruntime_err, to_pyvalue_err}, diff --git a/nautilus_core/adapters/src/databento/python/loader.rs b/nautilus_core/adapters/src/databento/python/loader.rs index abc971ab3a5f..4ee41e6c2719 100644 --- a/nautilus_core/adapters/src/databento/python/loader.rs +++ b/nautilus_core/adapters/src/databento/python/loader.rs @@ -15,6 +15,7 @@ use std::{collections::HashMap, path::PathBuf}; +use databento::dbn; use nautilus_core::{ffi::cvec::CVec, python::to_pyvalue_err}; use nautilus_model::{ data::{ diff --git a/nautilus_core/adapters/src/databento/python/mod.rs b/nautilus_core/adapters/src/databento/python/mod.rs index 5da249685115..1011a0b4c0d0 100644 --- a/nautilus_core/adapters/src/databento/python/mod.rs +++ b/nautilus_core/adapters/src/databento/python/mod.rs @@ -13,7 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -pub mod decode; pub mod enums; pub mod historical; pub mod live; @@ -33,13 +32,5 @@ pub fn databento(_: Python<'_>, m: &PyModule) -> PyResult<()> { 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 8d65004d1905..f424649cf589 100644 --- a/nautilus_core/adapters/src/databento/symbology.rs +++ b/nautilus_core/adapters/src/databento/symbology.rs @@ -13,6 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use databento::dbn; use dbn::Record; use indexmap::IndexMap; use nautilus_model::identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}; diff --git a/nautilus_core/adapters/src/databento/types.rs b/nautilus_core/adapters/src/databento/types.rs index 21057d6970b3..5f638648c284 100644 --- a/nautilus_core/adapters/src/databento/types.rs +++ b/nautilus_core/adapters/src/databento/types.rs @@ -15,6 +15,7 @@ use std::ffi::c_char; +use databento::dbn; use nautilus_core::time::UnixNanos; use nautilus_model::{ enums::OrderSide, From 0b79fc7777ecee7bbd6d1079229ed6699d820ad4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 19:10:18 +1100 Subject: [PATCH 42/71] Update DatabentoLoader --- .../adapters/src/databento/decode.rs | 8 ++--- .../adapters/databento/test_loaders.py | 31 ------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index aee32b7ffde0..476b8394ab1b 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -694,8 +694,8 @@ pub fn decode_instrument_def_msg_v1( instrument_id, ts_init, )?)), - 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (Bond)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX spot)"), _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char @@ -734,8 +734,8 @@ pub fn decode_instrument_def_msg( instrument_id, ts_init, )?)), - 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (BOND)"), - 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX_SPOT)"), + 'B' => anyhow::bail!("Unsupported `instrument_class` 'B' (Bond)"), + 'X' => anyhow::bail!("Unsupported `instrument_class` 'X' (FX spot)"), _ => anyhow::bail!( "Unsupported `instrument_class` '{}'", msg.instrument_class as u8 as char diff --git a/tests/integration_tests/adapters/databento/test_loaders.py b/tests/integration_tests/adapters/databento/test_loaders.py index d7326fd12c92..4f8d0de90bbb 100644 --- a/tests/integration_tests/adapters/databento/test_loaders.py +++ b/tests/integration_tests/adapters/databento/test_loaders.py @@ -85,37 +85,6 @@ def test_loader_definition_glbx_futures() -> None: assert instrument.ts_init == 1680451436501583647 -@pytest.mark.skip(reason="WIP: Future spreads not currently supported") -def test_loader_definition_glbx_futures_spread() -> None: - # Arrange - loader = DatabentoDataLoader() - path = DATABENTO_TEST_DATA_DIR / "definition-glbx-es-futspread.dbn.zst" - - # Act - data = loader.from_dbn_file(path) - - # Assert - assert len(data) == 2 - assert isinstance(data[0], FuturesContract) - assert isinstance(data[1], FuturesContract) - instrument = data[0] - assert instrument.id == InstrumentId.from_str("ESH5-ESM5.GLBX") - assert instrument.raw_symbol == Symbol("ESH5-ESM5") - assert instrument.asset_class == AssetClass.INDEX - assert instrument.instrument_class == InstrumentClass.FUTURE - assert instrument.quote_currency == USD - assert not instrument.is_inverse - assert instrument.underlying == "ES" - assert instrument.price_precision == 2 - assert instrument.price_increment == Price.from_str("0.05") - assert instrument.size_precision == 0 - assert instrument.size_increment == 1 - assert instrument.multiplier == 1 - assert instrument.lot_size == 1 - assert instrument.ts_event == 1690848000000000000 - assert instrument.ts_init == 1690848000000000000 - - def test_loader_definition_glbx_options() -> None: # Arrange loader = DatabentoDataLoader() From d8d4c07e905df2aab607d8ec8448c8d2280662c8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Mar 2024 21:55:50 +1100 Subject: [PATCH 43/71] Fix DatabaseConfig port JSON parsing for Redis --- RELEASES.md | 1 + nautilus_core/common/src/redis.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 427c4f8e4cc5..1aa0fab7830e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -17,6 +17,7 @@ None ### Fixes - Fixed JSON format for log file output (was missing `timestamp` and `trader\_id`) +- Fixed `DatabaseConfig` port JSON parsing for Redis (was always falling back to the default 6379) - Fixed `ChandeMomentumOscillator` indicator divide by zero error --- diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index c0c2f9f5fbad..97aaa841a30b 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -151,7 +151,7 @@ pub fn get_redis_url(config: &HashMap) -> String { let host = database .get("host") .map(|v| v.as_str().unwrap_or("127.0.0.1")); - let port = database.get("port").map(|v| v.as_str().unwrap_or("6379")); + let port = database.get("port").map(|v| v.as_u64().unwrap_or(6379)); let username = database .get("username") .map(|v| v.as_str().unwrap_or_default()); From fdf1ca8032247df7a3d0d350ffd82a2209e62d4b Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Sun, 17 Mar 2024 16:27:49 -0400 Subject: [PATCH 44/71] Improve Interactive Brokers client connectivity resilience (#1532) --- docs/integrations/ib.md | 12 +- .../interactive_brokers/client/account.py | 7 +- .../interactive_brokers/client/client.py | 209 ++++++++++------ .../interactive_brokers/client/common.py | 18 +- .../interactive_brokers/client/connection.py | 230 +++++++----------- .../interactive_brokers/client/error.py | 35 +-- .../interactive_brokers/client/market_data.py | 10 +- .../interactive_brokers/client/order.py | 6 +- .../adapters/interactive_brokers/data.py | 6 +- .../interactive_brokers/historic/client.py | 2 +- .../adapters/interactive_brokers/providers.py | 3 + .../interactive_brokers/client/test_client.py | 122 +++++----- .../client/test_client_account.py | 3 +- .../client/test_client_connection.py | 89 +++---- .../client/test_client_error.py | 19 +- .../adapters/interactive_brokers/conftest.py | 56 ++++- 16 files changed, 433 insertions(+), 394 deletions(-) diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index e1c97e96b661..3a669a17fd51 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -1,13 +1,13 @@ # Interactive Brokers -Interactive Brokers (IB) is a trading platform that allows trading across a wide range of financial instruments, including stocks, options, futures, currencies, bonds, funds, and cryptocurrencies. NautilusTrader offers an adapter to integrate with IB using their [Trader Workstation (TWS) API](https://interactivebrokers.github.io/tws-api/index.html) through their Python library, [ibapi](https://github.com/nautechsystems/ibapi). +Interactive Brokers (IB) is a trading platform that allows trading across a wide range of financial instruments, including stocks, options, futures, currencies, bonds, funds, and cryptocurrencies. NautilusTrader offers an adapter to integrate with IB using their [Trader Workstation (TWS) API](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/) through their Python library, [ibapi](https://github.com/nautechsystems/ibapi). -The TWS API serves as an interface to IB's standalone trading applications: TWS and IB Gateway. Both can be downloaded from the IB website. If you haven't installed TWS or IB Gateway yet, refer to the [Initial Setup](https://interactivebrokers.github.io/tws-api/initial_setup.html) guide. In NautilusTrader, you'll establish a connection to one of these applications via the `InteractiveBrokersClient`. +The TWS API serves as an interface to IB's standalone trading applications: TWS and IB Gateway. Both can be downloaded from the IB website. If you haven't installed TWS or IB Gateway yet, refer to the [Initial Setup](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#tws-download) guide. In NautilusTrader, you'll establish a connection to one of these applications via the `InteractiveBrokersClient`. -Alternatively, you can start with a [dockerized version](https://github.com/gnzsnz/ib-gateway-docker) of the IB Gateway, particularly useful when deploying trading strategies on a hosted cloud platform. This requires having [Docker](https://www.docker.com/) installed on your machine, along with the [docker](https://pypi.org/project/docker/) Python package, which NautilusTrader conveniently includes as an extra package. +Alternatively, you can start with a [dockerized version](https://github.com/gnzsnz/ib-gateway-docker) of the IB Gateway, which is particularly useful when deploying trading strategies on a hosted cloud platform. This requires having [Docker](https://www.docker.com/) installed on your machine, along with the [docker](https://pypi.org/project/docker/) Python package, which NautilusTrader conveniently includes as an extra package. ```{note} -The standalone TWS and IB Gateway applications necessitate manual input of username, password, and trading mode (live or paper) at startup. The dockerized version of the IB Gateway handles these steps programmatically. +The standalone TWS and IB Gateway applications require manually inputting username, password, and trading mode (live or paper) at startup. The dockerized version of the IB Gateway handles these steps programmatically. ``` ## Installation @@ -87,7 +87,7 @@ To troubleshoot TWS API incoming message issues, consider starting at the `Inter ## Instruments & Contracts -In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://interactivebrokers.github.io/tws-api/contracts.html). Contracts can be either a [basic contract](https://interactivebrokers.github.io/tws-api/classIBApi_1_1Contract.html) or a more [detailed](https://interactivebrokers.github.io/tws-api/classIBApi_1_1ContractDetails.html) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. +In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contracts). Contracts can be either a [basic contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-object) or a more [detailed](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-details) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. To search for contract information, use the [IB Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/). @@ -195,7 +195,7 @@ instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( ### Data Client -`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon connection, it configures the [market data type](https://interactivebrokers.github.io/tws-api/market_data_type.html) and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. This client can subscribe to and unsubscribe from various market data types, including quote ticks, trade ticks, and bars. +`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon connection, it configures the [market data type](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#delayed-market-data) and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. This client can subscribe to and unsubscribe from various market data types, including quote ticks, trade ticks, and bars. Configurable through `InteractiveBrokersDataClientConfig`, it allows adjustments for handling revised bars, trading hours preferences, and market data types (e.g., `IBMarketDataTypeEnum.REALTIME` or `IBMarketDataTypeEnum.DELAYED_FROZEN`). diff --git a/nautilus_trader/adapters/interactive_brokers/client/account.py b/nautilus_trader/adapters/interactive_brokers/client/account.py index 4a749a65d69b..f90084a9c37d 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/account.py +++ b/nautilus_trader/adapters/interactive_brokers/client/account.py @@ -157,9 +157,10 @@ def process_managed_accounts(self, *, accounts_list: str) -> None: """ self._account_ids = {a for a in accounts_list.split(",") if a} - if self._next_valid_order_id >= 0 and not self._is_ib_ready.is_set(): - self._log.info("`is_ib_ready` set by managedAccounts", LogColor.BLUE) - self._is_ib_ready.set() + self._log.debug(f"Managed accounts set: {self._account_ids}") + if self._next_valid_order_id >= 0 and not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` set by `managedAccounts`.", LogColor.BLUE) + self._is_ib_connected.set() def process_position( self, diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index ff009fa1dbbe..28a1df14c945 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -15,6 +15,7 @@ import asyncio import functools +import os from collections.abc import Callable from collections.abc import Coroutine from inspect import iscoroutinefunction @@ -43,15 +44,12 @@ from nautilus_trader.adapters.interactive_brokers.client.order import InteractiveBrokersClientOrderMixin from nautilus_trader.adapters.interactive_brokers.client.wrapper import InteractiveBrokersEWrapper from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE -from nautilus_trader.adapters.interactive_brokers.common import IBContract -from nautilus_trader.adapters.interactive_brokers.parsing.instruments import instrument_id_to_ib_contract from nautilus_trader.cache.cache import Cache from nautilus_trader.common.component import Component from nautilus_trader.common.component import LiveClock from nautilus_trader.common.component import MessageBus from nautilus_trader.common.enums import LogColor from nautilus_trader.model.identifiers import ClientId -from nautilus_trader.model.identifiers import InstrumentId # fmt: on @@ -91,8 +89,6 @@ def __init__( component_id=ClientId(f"{IB_VENUE.value}-{client_id:03d}"), component_name=f"{type(self).__name__}-{client_id:03d}", msgbus=msgbus, - # TODO: Config needs to be fully formed earlier than this - # config={"name": f"{type(self).__name__}-{client_id:03d}", "client_id": client_id}, ) # Config self._loop = loop @@ -109,45 +105,41 @@ def __init__( ), ) + # EClient Overrides + self._eclient.sendMsg = self.sendMsg + self._eclient.logRequest = self.logRequest + # Tasks - self._watch_dog_task: asyncio.Task | None = None + self._connection_watchdog_task: asyncio.Task | None = None self._tws_incoming_msg_reader_task: asyncio.Task | None = None - self._internal_msg_queue_task: asyncio.Task | None = None + self._internal_msg_queue_processor_task: asyncio.Task | None = None self._internal_msg_queue: asyncio.Queue = asyncio.Queue() # Event flags self._is_client_ready: asyncio.Event = asyncio.Event() - self._is_ib_ready: asyncio.Event = asyncio.Event() # Connectivity between IB and TWS + self._is_ib_connected: asyncio.Event = asyncio.Event() # Hot caches self.registered_nautilus_clients: set = set() self._event_subscriptions: dict[str, Callable] = {} - # Reset - self._reset() - self._request_id_seq: int = 10000 - # Subscriptions self._requests = Requests() self._subscriptions = Subscriptions() - # Overrides for EClient - self._eclient.sendMsg = self.sendMsg - self._eclient.logRequest = self.logRequest - # AccountMixin self._account_ids: set[str] = set() # ConnectionMixin - self._connection_attempt_counter: int = 0 - self._contract_for_probe: IBContract = instrument_id_to_ib_contract( - InstrumentId.from_str("EUR/CHF.IDEALPRO"), - ) + self._reconnect_attempts: int = 0 + self._max_reconnect_attempts: int | None = int(os.getenv("IB_MAX_RECONNECT_ATTEMPTS", 0)) + self._indefinite_reconnect: bool = False if self._max_reconnect_attempts else True + self._reconnect_delay: int = 5 # seconds # MarketDataMixin self._bar_type_to_last_bar: dict[str, BarData | None] = {} - # OrderMixing + # OrderMixin self._exec_id_details: dict[ str, dict[str, Execution | (CommissionReport | str)], @@ -155,26 +147,85 @@ def __init__( self._order_id_to_order_ref: dict[int, AccountOrderRef] = {} self._next_valid_order_id: int = -1 + # Start client + self._request_id_seq: int = 10000 + def _start(self) -> None: """ Start the client. + + This method is called when the client is first initialized and when the client + is reset. It sets up the client and starts the connection watchdog, incoming + message reader, and internal message queue processing tasks. + """ + if not self._loop.is_running(): + self._log.warning("Started when loop is not running.") + + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") + self._loop.run_until_complete(self._startup()) self._is_client_ready.set() + async def _startup(self): + try: + self._log.info(f"Starting InteractiveBrokersClient ({self._client_id})...") + await self._connect() + self._start_tws_incoming_msg_reader() + self._start_internal_msg_queue_processor() + self._eclient.startApi() + # TWS/Gateway will send a managedAccounts message upon successful connection, + # which will set the `_is_ib_connected` event. This typically takes a few + # seconds, so we wait for it here. + await asyncio.wait_for(self._is_ib_connected.wait(), 15) + self._start_connection_watchdog() + self._is_client_ready.set() + except asyncio.TimeoutError: + self._log.error("Client failed to initialize. Connection timeout.") + self._stop() + except Exception as e: + self._log.exception("Unhandled exception in client startup", e) + self._stop() + + def _start_tws_incoming_msg_reader(self) -> None: + """ + Start the incoming message reader task. + """ + if self._tws_incoming_msg_reader_task: + self._tws_incoming_msg_reader_task.cancel() + self._tws_incoming_msg_reader_task = self._create_task( + self._run_tws_incoming_msg_reader(), + ) + + def _start_internal_msg_queue_processor(self) -> None: + """ + Start the internal message queue processing task. + """ + if self._internal_msg_queue_processor_task: + self._internal_msg_queue_processor_task.cancel() + self._internal_msg_queue_processor_task = self._create_task( + self._run_internal_msg_queue_processor(), + ) + + def _start_connection_watchdog(self) -> None: + """ + Start the connection watchdog task. + """ + if self._connection_watchdog_task: + self._connection_watchdog_task.cancel() + self._connection_watchdog_task = self._create_task( + self._run_connection_watchdog(), + ) + def _stop(self) -> None: """ Stop the client and cancel running tasks. """ - if self.registered_nautilus_clients != set(): - self._log.warning( - f"Any registered Clients from {self.registered_nautilus_clients} will disconnect.", - ) - + self._log.info(f"Stopping InteractiveBrokersClient ({self._client_id})...") # Cancel tasks tasks = [ - self._watch_dog_task, + self._connection_watchdog_task, self._tws_incoming_msg_reader_task, - self._internal_msg_queue_task, + self._internal_msg_queue_processor_task, ] for task in tasks: if task and not task.cancelled(): @@ -183,62 +234,47 @@ def _stop(self) -> None: self._eclient.disconnect() self._is_client_ready.clear() self._account_ids = set() + for client in self.registered_nautilus_clients: + self._log.warning(f"Client {client} disconnected.") + self.registered_nautilus_clients = set() def _reset(self) -> None: """ - Reset the client state and restart connection watchdog. + Restart the client. """ + self._log.info(f"Resetting InteractiveBrokersClient ({self._client_id})...") self._stop() - self._eclient.reset() - - # Start the Watchdog - self._watch_dog_task = self._create_task(self._run_watch_dog()) + self._start() def _resume(self) -> None: """ - Resume the client and reset the connection attempt counter. + Resume the client and resubscribe to all subscriptions. """ + self._log.info(f"Resuming InteractiveBrokersClient ({self._client_id})...") self._is_client_ready.set() - self._connection_attempt_counter = 0 def _degrade(self) -> None: """ Degrade the client when connectivity is lost. """ + self._log.info(f"Degrading InteractiveBrokersClient ({self._client_id})...") self._is_client_ready.clear() self._account_ids = set() - def _start_client_tasks_and_tws_api(self) -> None: - """ - Start the incoming message reader and queue tasks, and initiate the start API - call to the EClient. - """ - if self._tws_incoming_msg_reader_task: - self._tws_incoming_msg_reader_task.cancel() - self._tws_incoming_msg_reader_task = self._create_task( - self._run_tws_incoming_msg_reader(), - ) - if self._internal_msg_queue_task: - self._internal_msg_queue_task.cancel() - self._internal_msg_queue_task = self._create_task( - self._run_internal_msg_queue(), - ) - self._eclient.startApi() - - async def _cancel_and_restart_subscriptions(self) -> None: + async def _resubscribe_all(self) -> None: """ - Attempt to cancel and restart all subscriptions. + Cancel and restart all subscriptions. """ + self._log.debug("Resubscribing all subscriptions...") for subscription in self._subscriptions.get_all(): try: subscription.cancel() if iscoroutinefunction(subscription.handle): await subscription.handle() else: - await self._loop.run_in_executor(None, subscription.handle) + await asyncio.to_thread(subscription.handle) except Exception as e: - # The exception is handled, so won't be further raised - self._log.exception("Failed subscription", e) + self._log.exception(f"Failed to resubscribe to {subscription}", e) async def wait_until_ready(self, timeout: int = 300) -> None: """ @@ -256,7 +292,7 @@ async def wait_until_ready(self, timeout: int = 300) -> None: except asyncio.TimeoutError as e: self._log.error(f"Client is not ready. {e}") - async def _run_watch_dog(self) -> None: + async def _run_connection_watchdog(self) -> None: """ Run a watchdog to monitor and manage the health of the socket connection. @@ -268,24 +304,27 @@ async def _run_watch_dog(self) -> None: try: while True: await asyncio.sleep(1) - if not self._eclient.isConnected(): - await self._reconnect() - - if not self._is_ib_ready.is_set(): - if self.is_running: - self._degrade() - continue - await self._probe_for_connectivity() + if not self._is_ib_connected.is_set() or not self._eclient.isConnected(): + self._log.error( + "Connection watchdog detects connection lost.", + ) + await self._handle_disconnection() + except asyncio.CancelledError: + self._log.debug("Client connection watchdog task was canceled.") - if self.is_degraded: - await self._cancel_and_restart_subscriptions() - self._resume() + async def _handle_disconnection(self) -> None: + """ + Handle the disconnection of the client from TWS/Gateway. + """ + if self.is_running: + self._degrade() + if not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `_handle_disconnection`.", LogColor.BLUE) + self._is_ib_connected.clear() + await asyncio.sleep(5) + await self._handle_reconnect() - if self.is_initialized and not self.is_running: - self._start() - except asyncio.CancelledError: - # The exception is handled, so won't be further raised - self._log.debug("Client `watch_dog` task was canceled.") + self._resume() def _create_task( self, @@ -409,7 +448,11 @@ async def _await_request(self, request: Request, timeout: int) -> Any | None: try: return await asyncio.wait_for(request.future, timeout) except asyncio.TimeoutError as e: - self._log.info(f"Request timed out for {request}") + self._log.warning(f"Request timed out for {request}. Ending request.") + self._end_request(request.req_id, success=False, exception=e) + return None + except ConnectionError as e: + self._log.error(f"Connection error during {request}. Ending request.") self._end_request(request.req_id, success=False, exception=e) return None @@ -449,11 +492,11 @@ async def _run_tws_incoming_msg_reader(self) -> None: Continuously read messages from TWS/Gateway and then put them in the internal message queue for processing. """ - self._log.debug("Client TWS incoming message reader starting...") + self._log.debug("Client TWS incoming message reader started.") buf = b"" try: while self._eclient.conn and self._eclient.conn.isConnected(): - data = await self._loop.run_in_executor(None, self._eclient.conn.recvMsg) + data = await asyncio.to_thread(self._eclient.conn.recvMsg) buf += data while buf: _, msg, buf = comm.read_msg(buf) @@ -469,14 +512,20 @@ async def _run_tws_incoming_msg_reader(self) -> None: except Exception as e: self._log.exception("Unhandled exception in Client TWS incoming message reader", e) finally: + if self._is_ib_connected.is_set() and not self.is_disposed: + self._log.debug( + "`_is_ib_connected` unset by `_run_tws_incoming_msg_reader`.", + LogColor.BLUE, + ) + self._is_ib_connected.clear() self._log.debug("Client TWS incoming message reader stopped.") - async def _run_internal_msg_queue(self) -> None: + async def _run_internal_msg_queue_processor(self) -> None: """ Continuously process messages from the internal incoming message queue. """ self._log.debug( - "Client internal message queue starting...", + "Client internal message queue started.", ) try: while ( @@ -498,7 +547,7 @@ async def _run_internal_msg_queue(self) -> None: ) ) finally: - self._eclient.disconnect() + self._log.debug("Client TWS incoming message reader stopped.") def _process_message(self, msg: str) -> bool: """ diff --git a/nautilus_trader/adapters/interactive_brokers/client/common.py b/nautilus_trader/adapters/interactive_brokers/client/common.py index 7b6235fdd514..1104a65a0d22 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/common.py +++ b/nautilus_trader/adapters/interactive_brokers/client/common.py @@ -493,22 +493,28 @@ class BaseMixin: _subscriptions: Subscriptions _event_subscriptions: dict[str, Callable] _eclient: EClient - _is_ib_ready: asyncio.Event + _is_ib_connected: asyncio.Event + _start: Callable + _startup: Callable + _reset: Callable + _stop: Callable + _resume: Callable _degrade: Callable _end_request: Callable _await_request: Callable _next_req_id: Callable - logAnswer: Callable - _reset: Callable + _resubscribe_all: Callable _create_task: Callable - _start_client_tasks_and_tws_api: Callable + logAnswer: Callable # Account accounts: Callable # Connection - _connection_attempt_counter: int - _contract_for_probe: IBContract + _reconnect_attempts: int + _reconnect_delay: int + _max_reconnect_attempts: int + _indefinite_reconnect: bool # MarketData _bar_type_to_last_bar: dict[str, BarData | None] diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index bac007b4065c..a009bff75239 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -26,6 +26,7 @@ from ibapi.server_versions import MIN_CLIENT_VER from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin +from nautilus_trader.common.enums import LogColor class InteractiveBrokersClientConnectionMixin(BaseMixin): @@ -34,26 +35,23 @@ class InteractiveBrokersClientConnectionMixin(BaseMixin): This class is responsible for establishing and maintaining the socket connection, handling server communication, monitoring the connection's health, and managing - reconnections. + reconnections. When a connection is established and the client finishes initializing, + the `_is_ib_connected` event is set, and if the connection is lost, the + `_is_ib_connected` event is cleared. """ - async def _establish_socket_connection(self) -> None: + async def _connect(self) -> None: """ - Establish the socket connection with TWS/Gateway. It initializes the connection, - connects the socket, sends and receives version information, and then sets up - the client. + Establish the socket connection with TWS/Gateway. - Raises - ------ - OSError - If an OSError occurs during the connection process. - Exception - For any other unexpected errors during the connection. + This initializes the connection, connects the socket, sends and receives version + information, and then sets a flag that the connection has been successfully + established. """ - self._initialize_connection_params() try: + self._initialize_connection_params() await self._connect_socket() self._eclient.setConnState(EClient.CONNECTING) await self._send_version_info() @@ -63,12 +61,55 @@ async def _establish_socket_connection(self) -> None: ) await self._receive_server_info() self._eclient.setConnState(EClient.CONNECTED) - self._start_client_tasks_and_tws_api() - self._log.debug("TWS API connection established successfully.") - except OSError as e: - self._handle_connection_error(e) + self._log.info( + f"Connected to Interactive Brokers (v{self._eclient.serverVersion_}) " + f"at {self._eclient.connTime.decode()} from {self._host}:{self._port} " + f"with client id: {self._client_id}.", + ) + except asyncio.CancelledError: + self._log.info("Connection cancelled.") + await self._disconnect() + except Exception as e: + self._log.error(f"Connection failed: {e}") + if self._eclient.wrapper: + self._eclient.wrapper.error(NO_VALID_ID, CONNECT_FAIL.code(), CONNECT_FAIL.msg()) + await self._handle_reconnect() + + async def _disconnect(self) -> None: + """ + Disconnect from TWS/Gateway and clear the `_is_ib_connected` flag. + """ + try: + self._eclient.disconnect() + if self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `_disconnect`.", LogColor.BLUE) + self._is_ib_connected.clear() + self._log.info("Disconnected from Interactive Brokers API.") except Exception as e: - self._log.exception("Unexpected error during connection", e) + self._log.error(f"Disconnection failed: {e}") + + async def _handle_reconnect(self) -> None: + """ + Attempt to reconnect to TWS/Gateway. + """ + while not self._is_ib_connected.is_set(): + if ( + not self._indefinite_reconnect + and self._reconnect_attempts > self._max_reconnect_attempts + ): + self._log.error("Max reconnection attempts reached. Connection failed.") + self._stop() + break + self._reconnect_attempts += 1 + self._log.info( + f"Attempt {self._reconnect_attempts}: Attempting to reconnect in {self._reconnect_delay} seconds...", + ) + await asyncio.sleep(self._reconnect_delay) + await self._startup() + await self._resubscribe_all() # should this not be done in _resume? + self._resume() + else: + self._reconnect_attempts = 0 def _initialize_connection_params(self) -> None: """ @@ -78,14 +119,10 @@ def _initialize_connection_params(self) -> None: the connection attempt counter. Logs the attempt information. """ + self._eclient.reset() self._eclient._host = self._host self._eclient._port = self._port self._eclient.clientId = self._client_id - self._connection_attempt_counter += 1 - self._log.info( - f"Attempt {self._connection_attempt_counter}: " - f"Connecting to {self._host}:{self._port} w/ id:{self._client_id}", - ) async def _connect_socket(self) -> None: """ @@ -96,7 +133,10 @@ async def _connect_socket(self) -> None: """ self._eclient.conn = Connection(self._host, self._port) - await self._loop.run_in_executor(None, self._eclient.conn.connect) + self._log.info( + f"Connecting to {self._host}:{self._port} with client id: {self._client_id}", + ) + await asyncio.to_thread(self._eclient.conn.connect) async def _send_version_info(self) -> None: """ @@ -113,10 +153,7 @@ async def _send_version_info(self) -> None: v100version += f" {self._eclient.connectionOptions}" msg = comm.make_msg(v100version) msg2 = str.encode(v100prefix, "ascii") + msg - await self._loop.run_in_executor( - None, - functools.partial(self._eclient.conn.sendMsg, msg2), - ) + await asyncio.to_thread(functools.partial(self._eclient.conn.sendMsg, msg2)) async def _receive_server_info(self) -> None: """ @@ -131,49 +168,33 @@ async def _receive_server_info(self) -> None: If the server version information is not received within the allotted retries. """ - connection_retries_remaining = 5 + retries_remaining = 5 fields: list[str] = [] - while len(fields) != 2 and connection_retries_remaining > 0: + while retries_remaining > 0: + buf = await asyncio.to_thread(self._eclient.conn.recvMsg) + if len(buf) > 0: + _, msg, _ = comm.read_msg(buf) + fields.extend(comm.read_fields(msg)) + else: + self._log.debug("Received empty buffer.") + + if len(fields) == 2: + self._process_server_version(fields) + break + + retries_remaining -= 1 + self._log.warning( + "Failed to receive server version information. " + f"Retries remaining: {retries_remaining}.", + ) await asyncio.sleep(1) - buf = await self._loop.run_in_executor(None, self._eclient.conn.recvMsg) - self._process_received_buffer(buf, connection_retries_remaining, fields) - - if len(fields) == 2: - self._process_server_version(fields) - else: - raise ConnectionError("Failed to receive server version information.") - - def _process_received_buffer( - self, - buf: bytes, - retries_remaining: int, - fields: list[str], - ) -> None: - """ - Process the received buffer from TWS API. Reads the received message and - extracts fields from it. Handles situations where the connection might be lost - or the received buffer is empty. - - Parameters - ---------- - buf : bytes - The received buffer from the server. - retries_remaining : int - The number of remaining retries for receiving the message. - fields : list[str] - The list to which the extracted fields will be appended. - """ - if not self._eclient.conn.isConnected() or retries_remaining <= 0: - self._log.warning("Disconnected. Resetting connection...") - self._reset() - return - if len(buf) > 0: - _, msg, _ = comm.read_msg(buf) - fields.extend(comm.read_fields(msg)) - else: - self._log.debug(f"Received empty buffer (retries_remaining={retries_remaining})") + if retries_remaining == 0: + raise ConnectionError( + "Max retry attempts reached. Failed to receive server version information.", + ) + self._log.info("") def _process_server_version(self, fields: list[str]) -> None: """ @@ -191,77 +212,6 @@ def _process_server_version(self, fields: list[str]) -> None: self._eclient.connTime = conn_time self._eclient.serverVersion_ = server_version self._eclient.decoder.serverVersion = server_version - self._log.debug(f"Connected to server version {server_version} at {conn_time}") - - async def _reconnect(self) -> None: - """ - Manage socket connectivity, including reconnection attempts and error handling. - Degrades the client if it's currently running and tries to re-establish the - socket connection. Waits for the Interactive Brokers readiness signal, logging - success or failure accordingly. - - Raises - ------ - asyncio.TimeoutError - If the connection attempt times out. - Exception - For general failures in re-establishing the connection. - - """ - if self.is_running: - self._degrade() - self._is_ib_ready.clear() - await asyncio.sleep(5) # Avoid too fast attempts - await self._establish_socket_connection() - try: - await asyncio.wait_for(self._is_ib_ready.wait(), 15) - self._log.info( - f"Connected to {self._host}:{self._port} w/ id:{self._client_id}", - ) - except asyncio.TimeoutError: - self._log.error( - f"Unable to connect to {self._host}:{self._port} w/ id:{self._client_id}", - ) - except Exception as e: - self._log.exception("Failed connection", e) - - async def _probe_for_connectivity(self) -> None: - """ - Perform a connectivity probe to TWS using a historical data request if the - client is degraded. - """ - # Probe connectivity. Sometime restored event will not be received from TWS without this - self._eclient.reqHistoricalData( - reqId=1, - contract=self._contract_for_probe, - endDateTime="", - durationStr="30 S", - barSizeSetting="5 secs", - whatToShow="MIDPOINT", - useRTH=False, - formatDate=2, - keepUpToDate=False, - chartOptions=[], - ) - await asyncio.sleep(15) - self._eclient.cancelHistoricalData(1) - - def _handle_connection_error(self, e): - """ - Handle any connection errors that occur during the connection setup. Logs the - error, notifies the wrapper of the connection failure, and disconnects the - client. - - Parameters - ---------- - e : Exception - The exception that occurred during the connection process. - - """ - if self._eclient.wrapper: - self._eclient.wrapper.error(NO_VALID_ID, CONNECT_FAIL.code(), CONNECT_FAIL.msg()) - self._eclient.disconnect() - self._log.error(f"Connection failed: {e}") def process_connection_closed(self) -> None: """ @@ -273,5 +223,7 @@ def process_connection_closed(self) -> None: """ for future in self._requests.get_futures(): if not future.done(): - future.set_exception(ConnectionError("Socket disconnect")) - self._eclient.reset() + future.set_exception(ConnectionError("Socket disconnected.")) + if self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` unset by `connectionClosed`.", LogColor.BLUE) + self._is_ib_connected.clear() diff --git a/nautilus_trader/adapters/interactive_brokers/client/error.py b/nautilus_trader/adapters/interactive_brokers/client/error.py index 09511226bf2b..bec85f64875f 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/error.py +++ b/nautilus_trader/adapters/interactive_brokers/client/error.py @@ -16,6 +16,7 @@ from inspect import iscoroutinefunction from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin +from nautilus_trader.common.enums import LogColor class InteractiveBrokersClientErrorMixin(BaseMixin): @@ -59,12 +60,8 @@ def _log_message( Indicates whether the message is a warning or an error. """ - msg_type = "Warning" if is_warning else "Error" - msg = f"{msg_type} {error_code} {req_id=}: {error_string}" - if is_warning: - self._log.info(msg) - else: - self._log.error(msg) + msg = f"{error_string} (code: {error_code}, {req_id=})." + self._log.warning(msg) if is_warning else self._log.error(msg) def process_error( self, @@ -92,6 +89,7 @@ def process_error( """ is_warning = error_code in self.WARNING_CODES or 2100 <= error_code < 2200 + error_string = error_string.replace("\n", " ") self._log_message(error_code, req_id, error_string, is_warning) if req_id != -1: @@ -104,12 +102,19 @@ def process_error( else: self._log.warning(f"Unhandled error: {error_code} for req_id {req_id}") elif error_code in self.CLIENT_ERRORS or error_code in self.CONNECTIVITY_LOST_CODES: - self._log.warning(f"Client or Connectivity Lost Error: {error_string}") - if self._is_ib_ready.is_set(): - self._is_ib_ready.clear() + if self._is_ib_connected.is_set(): + self._log.debug( + f"`_is_ib_connected` unset by code {error_code} in `_process_error`.", + LogColor.BLUE, + ) + self._is_ib_connected.clear() elif error_code in self.CONNECTIVITY_RESTORED_CODES: - if not self._is_ib_ready.is_set(): - self._is_ib_ready.set() + if not self._is_ib_connected.is_set(): + self._log.debug( + f"`_is_ib_connected` set by code {error_code} in `_process_error`.", + LogColor.BLUE, + ) + self._is_ib_connected.set() def _handle_subscription_error(self, req_id: int, error_code: int, error_string: str) -> None: """ @@ -141,9 +146,11 @@ def _handle_subscription_error(self, req_id: int, error_code: int, error_string: elif error_code == 10182: # Handle disconnection error self._log.warning(f"{error_code}: {error_string}") - if self._is_ib_ready.is_set(): - self._log.info(f"`is_ib_ready` cleared by {subscription.name}") - self._is_ib_ready.clear() + if self._is_ib_connected.is_set(): + self._log.info( + f"`_is_ib_connected` unset by {subscription.name} in `_handle_subscription_error`.", + ) + self._is_ib_connected.clear() else: # Log unknown subscription errors self._log.warning( diff --git a/nautilus_trader/adapters/interactive_brokers/client/market_data.py b/nautilus_trader/adapters/interactive_brokers/client/market_data.py index f12b71e8a9b7..0fc1b4bde804 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/market_data.py +++ b/nautilus_trader/adapters/interactive_brokers/client/market_data.py @@ -16,6 +16,7 @@ import functools from collections.abc import Callable from decimal import Decimal +from inspect import iscoroutinefunction from typing import Any import pandas as pd @@ -35,7 +36,6 @@ from nautilus_trader.adapters.interactive_brokers.parsing.data import timedelta_to_duration_str from nautilus_trader.adapters.interactive_brokers.parsing.data import what_to_show from nautilus_trader.adapters.interactive_brokers.parsing.instruments import ib_contract_to_instrument_id -from nautilus_trader.common.enums import LogColor from nautilus_trader.core.data import Data from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType @@ -124,7 +124,10 @@ async def _subscribe( ) if not subscription: return None - subscription.handle() + if iscoroutinefunction(subscription.handle): + await subscription.handle() + else: + subscription.handle() return subscription else: self._log.info(f"Subscription already exists for {subscription}") @@ -793,9 +796,6 @@ def process_historical_data_end(self, *, req_id: int, start: str, end: str) -> N Mark the end of receiving historical bars. """ self._end_request(req_id) - if req_id == 1 and not self._is_ib_ready.is_set(): # probe successful - self._log.info(f"`is_ib_ready` set by historicalDataEnd {req_id=}", LogColor.BLUE) - self._is_ib_ready.set() def process_historical_data_update(self, *, req_id: int, bar: BarData) -> None: """ diff --git a/nautilus_trader/adapters/interactive_brokers/client/order.py b/nautilus_trader/adapters/interactive_brokers/client/order.py index 806efecad472..1fd496422f5c 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/order.py +++ b/nautilus_trader/adapters/interactive_brokers/client/order.py @@ -152,9 +152,9 @@ def process_next_valid_id(self, *, order_id: int) -> None: """ self._next_valid_order_id = max(self._next_valid_order_id, order_id, 101) - if self.accounts() and not self._is_ib_ready.is_set(): - self._log.info("`is_ib_ready` set by nextValidId", LogColor.BLUE) - self._is_ib_ready.set() + if self.accounts() and not self._is_ib_connected.is_set(): + self._log.debug("`_is_ib_connected` set by `nextValidId`.", LogColor.BLUE) + self._is_ib_connected.set() def process_open_order( self, diff --git a/nautilus_trader/adapters/interactive_brokers/data.py b/nautilus_trader/adapters/interactive_brokers/data.py index 3280ff737169..dd4384ced322 100644 --- a/nautilus_trader/adapters/interactive_brokers/data.py +++ b/nautilus_trader/adapters/interactive_brokers/data.py @@ -94,11 +94,7 @@ def __init__( cache=cache, clock=clock, instrument_provider=instrument_provider, - config=config, # TODO: Config needs to be fully formed earlier than this - # config={ - # "name": f"{type(self).__name__}-{ibg_client_id:03d}", - # "client_id": ibg_client_id, - # }, + config=config, ) self._client = client self._handle_revised_bars = config.handle_revised_bars diff --git a/nautilus_trader/adapters/interactive_brokers/historic/client.py b/nautilus_trader/adapters/interactive_brokers/historic/client.py index 2770b1ff56d9..8237c00ed38c 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/client.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/client.py @@ -87,7 +87,7 @@ def __init__( async def _connect(self) -> None: # Connect client - await self._client.wait_until_ready() + self._client.start() self._client.registered_nautilus_clients.add(1) # Set Market Data Type diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index 4f0e43e9c183..c804fc3406f7 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -102,6 +102,9 @@ async def get_contract_details( ) -> list[ContractDetails]: try: details = await self._client.get_contract_details(contract=contract) + if not details: + self._log.error(f"No contract details returned for {contract}.") + return [] [qualified] = details self._log.info( f"Contract qualified for {qualified.contract.localSymbol}." diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py index ca55104fa357..7b3eaea4ec10 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client.py @@ -21,73 +21,95 @@ import pytest +from nautilus_trader.test_kit.functions import ensure_all_tasks_completed from nautilus_trader.test_kit.functions import eventually def test_start(ib_client): - # Arrange, Act - ib_client._start() + # Arrange + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + + # Act + ib_client.start() # Assert assert ib_client._is_client_ready.is_set() -def test_start_client_tasks_and_tws_api(ib_client): +def test_start_tasks(ib_client): # Arrange + ib_client._eclient = MagicMock() ib_client._tws_incoming_msg_reader_task = None ib_client._internal_msg_queue_task = None - ib_client._eclient.startApi = Mock() + ib_client._connection_watchdog_task = None # Act - ib_client._start_client_tasks_and_tws_api() + ib_client._start_tws_incoming_msg_reader() + ib_client._start_internal_msg_queue_processor() + ib_client._start_connection_watchdog() # Assert - assert ib_client._tws_incoming_msg_reader_task - assert ib_client._internal_msg_queue_task - assert ib_client._eclient.startApi.called + # Tasks should be running if there's a (simulated) connection + assert not ib_client._tws_incoming_msg_reader_task.done() + assert not ib_client._internal_msg_queue_processor_task.done() + assert not ib_client._connection_watchdog_task.done() def test_stop(ib_client): # Arrange - ib_client._start_client_tasks_and_tws_api() - ib_client._eclient.disconnect = Mock() + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + ib_client.start() # Act - ib_client._stop() + ib_client.stop() + ensure_all_tasks_completed() # Assert - assert ib_client._watch_dog_task.cancel() - assert ib_client._tws_incoming_msg_reader_task.cancel() - assert ib_client._internal_msg_queue_task.cancel() - assert ib_client._eclient.disconnect.called + assert ib_client.is_stopped + assert ib_client._connection_watchdog_task.done() + assert ib_client._tws_incoming_msg_reader_task.done() + assert ib_client._internal_msg_queue_processor_task.done() assert not ib_client._is_client_ready.is_set() + assert len(ib_client.registered_nautilus_clients) == 0 def test_reset(ib_client): # Arrange ib_client._stop = Mock() - ib_client._eclient.reset = Mock() + ib_client._start = Mock() # Act - ib_client._reset() + ib_client.reset() # Assert assert ib_client._stop.called - assert ib_client._eclient.reset.called - assert ib_client._watch_dog_task + assert ib_client._start.called + + +def test_resume(ib_client_running): + # Arrange, Act, Assert + ib_client_running._degrade() + + # Act + ib_client_running._resume() + + # Assert + assert ib_client_running._is_client_ready.is_set() -def test_resume(ib_client): +def test_degrade(ib_client_running): # Arrange - ib_client._is_client_ready.clear() - ib_client._connection_attempt_counter = 1 # Act - ib_client._resume() + ib_client_running._degrade() # Assert - assert ib_client._is_client_ready.is_set() - assert ib_client._connection_attempt_counter == 0 + assert not ib_client_running._is_client_ready.is_set() + assert len(ib_client_running._account_ids) == 0 @pytest.mark.asyncio @@ -142,46 +164,29 @@ def test_next_req_id(ib_client): @pytest.mark.asyncio -async def test_wait_until_ready(ib_client): +async def test_wait_until_ready(ib_client_running): # Arrange - ib_client._is_client_ready = Mock() - ib_client._is_client_ready.is_set.return_value = True # Act - await ib_client.wait_until_ready() + await ib_client_running.wait_until_ready() # Assert - # Assert wait was not called since is_client_ready is already set - ib_client._is_client_ready.wait.assert_not_called() + assert True @pytest.mark.asyncio -async def test_run_watch_dog_reconnect(ib_client): +async def test_run_connection_watchdog_reconnect(ib_client): # Arrange + ib_client._is_ib_connected.clear() ib_client._eclient = MagicMock() ib_client._eclient.isConnected.return_value = False - ib_client._reconnect = AsyncMock(side_effect=asyncio.CancelledError) - - # Act - await ib_client._run_watch_dog() - - # Assert - ib_client._reconnect.assert_called() - - -@pytest.mark.asyncio -async def test_run_watch_dog_probe(ib_client): - # Arrange - ib_client._eclient = MagicMock() - ib_client._eclient.isConnected.return_value = True - ib_client._is_ib_ready.clear() - ib_client._probe_for_connectivity = AsyncMock(side_effect=asyncio.CancelledError) + ib_client._handle_disconnection = AsyncMock(side_effect=asyncio.CancelledError) # Act - await ib_client._run_watch_dog() + await ib_client._run_connection_watchdog() # Assert - ib_client._probe_for_connectivity.assert_called() + ib_client._handle_disconnection.assert_called() @pytest.mark.asyncio @@ -194,9 +199,7 @@ async def test_run_tws_incoming_msg_reader(ib_client): with patch("ibapi.comm.read_msg", side_effect=[(None, msg, b"") for msg in test_messages]): # Act - ib_client._tws_incoming_msg_reader_task = ib_client._create_task( - ib_client._run_tws_incoming_msg_reader(), - ) + ib_client._start_tws_incoming_msg_reader() await eventually(lambda: ib_client._internal_msg_queue.qsize() == len(test_messages)) # Assert @@ -205,18 +208,15 @@ async def test_run_tws_incoming_msg_reader(ib_client): @pytest.mark.asyncio -async def test_run_internal_msg_queue(ib_client): +async def test_run_internal_msg_queue(ib_client_running): # Arrange test_messages = [b"test message 1", b"test message 2"] for msg in test_messages: - ib_client._internal_msg_queue.put_nowait(msg) - ib_client._process_message = Mock() + ib_client_running._internal_msg_queue.put_nowait(msg) + ib_client_running._process_message = Mock() # Act - ib_client._internal_msg_queue_task = ib_client._create_task( - ib_client._run_internal_msg_queue(), - ) # Assert - await eventually(lambda: ib_client._process_message.call_count == len(test_messages)) - assert ib_client._internal_msg_queue.qsize() == 0 + await eventually(lambda: ib_client_running._process_message.call_count == len(test_messages)) + assert ib_client_running._internal_msg_queue.qsize() == 0 diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py index 10c500a3f239..74ef547d1a38 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_account.py @@ -64,7 +64,8 @@ async def test_process_account_id(ib_client): ] with patch("ibapi.comm.read_msg", side_effect=[(None, msg, b"") for msg in test_messages]): # Act - ib_client._start_client_tasks_and_tws_api() + ib_client._start_tws_incoming_msg_reader() + ib_client._start_internal_msg_queue_processor() # Assert await eventually(lambda: "DU1234567" in ib_client.accounts()) diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py index 20fde18b9fbf..12b7e3d904c1 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_connection.py @@ -1,63 +1,54 @@ -# ------------------------------------------------------------------------------------------------- -# 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. -# ------------------------------------------------------------------------------------------------- - +import asyncio from unittest.mock import AsyncMock -from unittest.mock import Mock -from unittest.mock import patch +from unittest.mock import MagicMock import pytest -from ibapi.client import EClient +from ibapi.common import NO_VALID_ID +from ibapi.errors import CONNECT_FAIL @pytest.mark.asyncio -async def test_establish_socket_connection(ib_client): - # Arrange - ib_client._eclient.connState = EClient.DISCONNECTED - ib_client._tws_incoming_msg_reader_task = None - ib_client._internal_msg_queue_task = None - ib_client._initialize_connection_params = Mock() +async def test_connect_success(ib_client): + ib_client._initialize_connection_params = MagicMock() ib_client._connect_socket = AsyncMock() ib_client._send_version_info = AsyncMock() ib_client._receive_server_info = AsyncMock() - ib_client._eclient.serverVersion = Mock() - ib_client._eclient.wrapper = Mock() - ib_client._eclient.startApi = Mock() - ib_client._eclient.conn = Mock() - ib_client._eclient.conn.isConnected = Mock(return_value=True) + ib_client._eclient.connTime = MagicMock() + ib_client._eclient.setConnState = MagicMock() + + await ib_client._connect() + + ib_client._initialize_connection_params.assert_called_once() + ib_client._connect_socket.assert_awaited_once() + ib_client._send_version_info.assert_awaited_once() + ib_client._receive_server_info.assert_awaited_once() + ib_client._eclient.setConnState.assert_called_with(ib_client._eclient.CONNECTED) + + +@pytest.mark.asyncio +async def test_connect_cancelled(ib_client): + ib_client._initialize_connection_params = MagicMock() + ib_client._connect_socket = AsyncMock(side_effect=asyncio.CancelledError()) + ib_client._disconnect = AsyncMock() - # Act - await ib_client._establish_socket_connection() + await ib_client._connect() - # Assert - assert ib_client._eclient.isConnected() - assert ib_client._tws_incoming_msg_reader_task - assert ib_client._internal_msg_queue_task - ib_client._eclient.startApi.assert_called_once() + ib_client._disconnect.assert_awaited_once() @pytest.mark.asyncio -async def test_connect_socket(ib_client): - # Arrange - with patch( - "nautilus_trader.adapters.interactive_brokers.client.connection.Connection", - ) as MockConnection: - mock_connection_instance = MockConnection.return_value - mock_connection_instance.connect = Mock() - - # Act - await ib_client._connect_socket() - - # Assert - mock_connection_instance.connect.assert_called_once() +async def test_connect_fail(ib_client): + ib_client._initialize_connection_params = MagicMock() + ib_client._connect_socket = AsyncMock(side_effect=Exception("Connection failed")) + ib_client._disconnect = AsyncMock() + ib_client._handle_reconnect = AsyncMock() + ib_client._eclient.wrapper.error = MagicMock() + + await ib_client._connect() + + ib_client._eclient.wrapper.error.assert_called_with( + NO_VALID_ID, + CONNECT_FAIL.code(), + CONNECT_FAIL.msg(), + ) + ib_client._handle_reconnect.assert_awaited_once() diff --git a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py index 573a331d29c5..764531510cb9 100644 --- a/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py +++ b/tests/integration_tests/adapters/interactive_brokers/client/test_client_error.py @@ -16,10 +16,13 @@ import functools from unittest.mock import Mock +import pytest + +@pytest.mark.asyncio def test_ib_is_ready_by_notification_1101(ib_client): # Arrange - ib_client._is_ib_ready.clear() + ib_client._is_ib_connected.clear() # Act ib_client.process_error( @@ -29,12 +32,12 @@ def test_ib_is_ready_by_notification_1101(ib_client): ) # Assert - assert ib_client._is_ib_ready.is_set() + assert ib_client._is_ib_connected.is_set() def test_ib_is_ready_by_notification_1102(ib_client): # Arrange - ib_client._is_ib_ready.clear() + ib_client._is_ib_connected.clear() # Act ib_client.process_error( @@ -44,13 +47,13 @@ def test_ib_is_ready_by_notification_1102(ib_client): ) # Assert - assert ib_client._is_ib_ready.is_set() + assert ib_client._is_ib_connected.is_set() def test_ib_is_not_ready_by_error_10182(ib_client): # Arrange req_id = 6 - ib_client._is_ib_ready.set() + ib_client._is_ib_connected.set() ib_client._subscriptions.add(req_id, "EUR.USD", ib_client._eclient.reqHistoricalData, {}) # Act @@ -61,13 +64,13 @@ def test_ib_is_not_ready_by_error_10182(ib_client): ) # Assert - assert not ib_client._is_ib_ready.is_set() + assert not ib_client._is_ib_connected.is_set() def test_ib_is_not_ready_by_error_10189(ib_client): # Arrange req_id = 6 - ib_client._is_ib_ready.set() + ib_client._is_ib_connected.set() ib_client._subscriptions.add( req_id=req_id, name="EUR.USD", @@ -91,4 +94,4 @@ def test_ib_is_not_ready_by_error_10189(ib_client): ) # Assert - assert not ib_client._is_ib_ready.is_set() + assert not ib_client._is_ib_connected.is_set() diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index a29d79aded6b..e182892f4e52 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- +import asyncio +from unittest.mock import AsyncMock +from unittest.mock import MagicMock import pytest @@ -28,6 +31,7 @@ from nautilus_trader.model.events import AccountState from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import Venue +from nautilus_trader.test_kit.functions import ensure_all_tasks_completed from nautilus_trader.test_kit.stubs.events import TestEventStubs from tests.integration_tests.adapters.interactive_brokers.test_kit import IBTestContractStubs @@ -35,6 +39,15 @@ # fmt: on +@pytest.fixture() +def event_loop(): + loop = asyncio.get_event_loop() + asyncio.set_event_loop(loop) + loop.set_debug(True) + yield loop + ensure_all_tasks_completed() + + @pytest.fixture() def venue(): return IB_VENUE @@ -73,9 +86,9 @@ def exec_client_config(): @pytest.fixture() -def ib_client(data_client_config, loop, msgbus, cache, clock): +def ib_client(data_client_config, event_loop, msgbus, cache, clock): client = InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -84,7 +97,18 @@ def ib_client(data_client_config, loop, msgbus, cache, clock): client_id=data_client_config.ibg_client_id, ) yield client - client._stop() + if client.is_running: + client._stop() + + +@pytest.fixture() +def ib_client_running(ib_client): + ib_client._is_ib_connected.set() + ib_client._connect = AsyncMock() + ib_client._eclient = MagicMock() + ib_client._account_ids = {"DU123456,"} + ib_client.start() + yield ib_client @pytest.fixture() @@ -96,11 +120,11 @@ def instrument_provider(ib_client): @pytest.fixture() -def data_client(mocker, data_client_config, venue, loop, msgbus, cache, clock): +def data_client(mocker, data_client_config, venue, event_loop, msgbus, cache, clock): mocker.patch( "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", return_value=InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -110,23 +134,27 @@ def data_client(mocker, data_client_config, venue, loop, msgbus, cache, clock): ), ) client = InteractiveBrokersLiveDataClientFactory.create( - loop=loop, + loop=event_loop, name=venue.value, config=data_client_config, msgbus=msgbus, cache=cache, clock=clock, ) - client._client.start() + client._client._is_ib_connected.set() + client._client._connect = AsyncMock() + client._client._eclient = MagicMock() + client._client._account_ids = {"DU123456,"} + # client._client.start() return client @pytest.fixture() -def exec_client(mocker, exec_client_config, venue, loop, msgbus, cache, clock): +def exec_client(mocker, exec_client_config, venue, event_loop, msgbus, cache, clock): mocker.patch( "nautilus_trader.adapters.interactive_brokers.factories.get_cached_ib_client", return_value=InteractiveBrokersClient( - loop=loop, + loop=event_loop, msgbus=msgbus, cache=cache, clock=clock, @@ -136,16 +164,18 @@ def exec_client(mocker, exec_client_config, venue, loop, msgbus, cache, clock): ), ) client = InteractiveBrokersLiveExecClientFactory.create( - loop=loop, + loop=event_loop, name=venue.value, config=exec_client_config, msgbus=msgbus, cache=cache, clock=clock, ) - client._client.start() - client._client.process_managed_accounts(accounts_list="DU123456,") - client._client.process_next_valid_id(order_id=1) + client._client._is_ib_connected.set() + client._client._connect = AsyncMock() + client._client._eclient = MagicMock() + client._client._account_ids = {"DU123456,"} + # client._client.start() return client From 2fc57070ffc44c1ac9d239267e38be5440fe4da8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 07:42:51 +1100 Subject: [PATCH 45/71] Standardize PyResult at Python interface --- nautilus_core/Cargo.lock | 1 - nautilus_core/Cargo.toml | 2 +- .../accounting/src/python/transformer.rs | 6 +- .../adapters/src/databento/bin/sandbox.rs | 3 +- .../adapters/src/databento/python/enums.rs | 17 ++- .../src/databento/python/historical.rs | 11 +- .../adapters/src/databento/python/live.rs | 5 +- .../adapters/src/databento/python/loader.rs | 137 ++++++++++-------- .../model/src/python/events/account/state.rs | 2 +- .../model/src/python/types/balance.rs | 14 +- 10 files changed, 111 insertions(+), 87 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 8584332e521d..53cc2ad6cf1b 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -3292,7 +3292,6 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ - "anyhow", "cfg-if", "indoc", "libc", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 1ae2e806e592..2e9f2fc8b5ea 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -32,7 +32,7 @@ indexmap = { version = "2.2.5", features = ["serde"] } itoa = "1.0.10" once_cell = "1.19.0" log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } -pyo3 = { version = "0.20.3", features = ["anyhow", "rust_decimal"] } +pyo3 = { version = "0.20.3", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" redis = { version = "0.25.2", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } diff --git a/nautilus_core/accounting/src/python/transformer.rs b/nautilus_core/accounting/src/python/transformer.rs index 9e9bcdd946f4..f02acdb9125b 100644 --- a/nautilus_core/accounting/src/python/transformer.rs +++ b/nautilus_core/accounting/src/python/transformer.rs @@ -34,7 +34,8 @@ pub fn cash_account_from_account_events( return Err(to_pyvalue_err("No account events")); } let init_event = account_events[0].clone(); - let mut cash_account = CashAccount::new(init_event, calculate_account_state)?; + let mut cash_account = + CashAccount::new(init_event, calculate_account_state).map_err(to_pyvalue_err)?; for event in account_events.iter().skip(1) { cash_account.apply(event.clone()); } @@ -56,7 +57,8 @@ pub fn margin_account_from_account_events( return Err(to_pyvalue_err("No account events")); } let init_event = account_events[0].clone(); - let mut margin_account = MarginAccount::new(init_event, calculate_account_state)?; + let mut margin_account = + MarginAccount::new(init_event, calculate_account_state).map_err(to_pyvalue_err)?; for event in account_events.iter().skip(1) { margin_account.apply(event.clone()); } diff --git a/nautilus_core/adapters/src/databento/bin/sandbox.rs b/nautilus_core/adapters/src/databento/bin/sandbox.rs index 27e9ec85151b..b9637d7a1263 100644 --- a/nautilus_core/adapters/src/databento/bin/sandbox.rs +++ b/nautilus_core/adapters/src/databento/bin/sandbox.rs @@ -1,8 +1,7 @@ use std::env; -use databento::dbn::{MboMsg, TradeMsg}; use databento::{ - dbn::{Dataset::GlbxMdp3, SType, Schema}, + dbn::{Dataset::GlbxMdp3, MboMsg, SType, Schema, TradeMsg}, live::Subscription, LiveClient, }; diff --git a/nautilus_core/adapters/src/databento/python/enums.rs b/nautilus_core/adapters/src/databento/python/enums.rs index db045cca0f5d..40006e676489 100644 --- a/nautilus_core/adapters/src/databento/python/enums.rs +++ b/nautilus_core/adapters/src/databento/python/enums.rs @@ -15,6 +15,7 @@ use std::str::FromStr; +use nautilus_core::python::to_pyvalue_err; use pyo3::{prelude::*, types::PyType, PyTypeInfo}; use crate::databento::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction}; @@ -22,9 +23,9 @@ use crate::databento::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAc #[pymethods] impl DatabentoStatisticType { #[new] - fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value) + Self::py_from_str(t, value).map_err(to_pyvalue_err) } fn __hash__(&self) -> isize { @@ -63,10 +64,10 @@ impl DatabentoStatisticType { #[classmethod] #[pyo3(name = "from_str")] - fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + fn py_from_str(_: &PyType, data: &PyAny) -> PyResult { let data_str: &str = data.str().and_then(|s| s.extract())?; let tokenized = data_str.to_uppercase(); - Self::from_str(&tokenized).map_err(anyhow::Error::new) + Self::from_str(&tokenized).map_err(to_pyvalue_err) } #[classattr] #[pyo3(name = "OPENING_PRICE")] @@ -150,9 +151,9 @@ impl DatabentoStatisticType { #[pymethods] impl DatabentoStatisticUpdateAction { #[new] - fn py_new(py: Python<'_>, value: &PyAny) -> anyhow::Result { + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { let t = Self::type_object(py); - Self::py_from_str(t, value) + Self::py_from_str(t, value).map_err(to_pyvalue_err) } fn __hash__(&self) -> isize { @@ -191,10 +192,10 @@ impl DatabentoStatisticUpdateAction { #[classmethod] #[pyo3(name = "from_str")] - fn py_from_str(_: &PyType, data: &PyAny) -> anyhow::Result { + fn py_from_str(_: &PyType, data: &PyAny) -> PyResult { let data_str: &str = data.str().and_then(|s| s.extract())?; let tokenized = data_str.to_uppercase(); - Self::from_str(&tokenized).map_err(anyhow::Error::new) + Self::from_str(&tokenized).map_err(to_pyvalue_err) } #[classattr] #[pyo3(name = "ADDED")] diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index aaec96b90685..1d92bd51d5e4 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -60,13 +60,16 @@ pub struct DatabentoHistoricalClient { #[pymethods] impl DatabentoHistoricalClient { #[new] - fn py_new(key: String, publishers_path: &str) -> anyhow::Result { + fn py_new(key: String, publishers_path: &str) -> PyResult { let client = databento::HistoricalClient::builder() - .key(key.clone())? - .build()?; + .key(key.clone()) + .map_err(to_pyvalue_err)? + .build() + .map_err(to_pyvalue_err)?; let file_content = fs::read_to_string(publishers_path)?; - let publishers_vec: Vec = serde_json::from_str(&file_content)?; + let publishers_vec: Vec = + serde_json::from_str(&file_content).map_err(to_pyvalue_err)?; let publisher_venue_map = publishers_vec .into_iter() diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index e37e6158dfa2..ef0a99135bfc 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -120,9 +120,10 @@ fn call_python(py: Python, callback: &PyObject, py_obj: PyObject) -> PyResult<() #[pymethods] impl DatabentoLiveClient { #[new] - pub fn py_new(key: String, dataset: String, publishers_path: String) -> anyhow::Result { + pub fn py_new(key: String, dataset: String, publishers_path: String) -> PyResult { let publishers_json = fs::read_to_string(publishers_path)?; - let publishers_vec: Vec = serde_json::from_str(&publishers_json)?; + let publishers_vec: Vec = + serde_json::from_str(&publishers_json).map_err(to_pyvalue_err)?; let publisher_venue_map = publishers_vec .into_iter() .map(|p| (p.publisher_id, Venue::from(p.venue.as_str()))) diff --git a/nautilus_core/adapters/src/databento/python/loader.rs b/nautilus_core/adapters/src/databento/python/loader.rs index 4ee41e6c2719..7890b2640fdd 100644 --- a/nautilus_core/adapters/src/databento/python/loader.rs +++ b/nautilus_core/adapters/src/databento/python/loader.rs @@ -43,9 +43,9 @@ impl DatabentoDataLoader { } #[pyo3(name = "load_publishers")] - fn py_load_publishers(&mut self, path: String) -> anyhow::Result<()> { + fn py_load_publishers(&mut self, path: String) -> PyResult<()> { let path_buf = PathBuf::from(path); - self.load_publishers(path_buf) + self.load_publishers(path_buf).map_err(to_pyvalue_err) } #[must_use] @@ -72,14 +72,17 @@ impl DatabentoDataLoader { } #[pyo3(name = "schema_for_file")] - fn py_schema_for_file(&self, path: String) -> anyhow::Result> { + fn py_schema_for_file(&self, path: String) -> PyResult> { self.schema_from_file(PathBuf::from(path)) + .map_err(to_pyvalue_err) } #[pyo3(name = "load_instruments")] - fn py_load_instruments(&mut self, py: Python, path: String) -> anyhow::Result { + fn py_load_instruments(&mut self, py: Python, path: String) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_definition_records(path_buf)?; + let iter = self + .read_definition_records(path_buf) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -103,9 +106,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -116,7 +121,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -130,15 +135,13 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + 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) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_order_book_depth10")] @@ -146,9 +149,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -159,7 +164,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -172,11 +177,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_quotes")] @@ -185,13 +192,11 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + let iter = self + .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 { @@ -202,7 +207,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -216,15 +221,13 @@ impl DatabentoDataLoader { path: String, instrument_id: Option, include_trades: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::( - path_buf, - instrument_id, - include_trades.unwrap_or(false), - )?; + 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) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_tbbo_trades")] @@ -232,9 +235,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -244,7 +249,7 @@ impl DatabentoDataLoader { data.push(trade); } } - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -257,11 +262,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_trades")] @@ -269,9 +276,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -282,7 +291,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -295,11 +304,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_bars")] @@ -307,9 +318,11 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { @@ -320,7 +333,7 @@ impl DatabentoDataLoader { } } Ok((None, _)) => continue, - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -333,11 +346,13 @@ impl DatabentoDataLoader { py: Python, path: String, instrument_id: Option, - ) -> anyhow::Result { + ) -> PyResult { let path_buf = PathBuf::from(path); - let iter = self.read_records::(path_buf, instrument_id, false)?; + let iter = self + .read_records::(path_buf, instrument_id, false) + .map_err(to_pyvalue_err)?; - exhaust_data_iter_to_pycapsule(py, iter) + exhaust_data_iter_to_pycapsule(py, iter).map_err(to_pyvalue_err) } #[pyo3(name = "load_imbalance")] @@ -345,15 +360,17 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_imbalance_records::(path_buf, instrument_id)?; + let iter = self + .read_imbalance_records::(path_buf, instrument_id) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { match result { Ok(item) => data.push(item), - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } @@ -365,15 +382,17 @@ impl DatabentoDataLoader { &self, path: String, instrument_id: Option, - ) -> anyhow::Result> { + ) -> PyResult> { let path_buf = PathBuf::from(path); - let iter = self.read_statistics_records::(path_buf, instrument_id)?; + let iter = self + .read_statistics_records::(path_buf, instrument_id) + .map_err(to_pyvalue_err)?; let mut data = Vec::new(); for result in iter { match result { Ok(item) => data.push(item), - Err(e) => return Err(e), + Err(e) => return Err(to_pyvalue_err(e)), } } diff --git a/nautilus_core/model/src/python/events/account/state.rs b/nautilus_core/model/src/python/events/account/state.rs index 7c458dfda3d6..0216dd64e60f 100644 --- a/nautilus_core/model/src/python/events/account/state.rs +++ b/nautilus_core/model/src/python/events/account/state.rs @@ -164,7 +164,7 @@ impl AccountState { UUID4::from_str(event_id).unwrap(), ts_event, ts_init, - Some(Currency::from_str(base_currency)?), + Some(Currency::from_str(base_currency).map_err(to_pyvalue_err)?), ) .unwrap(); Ok(account) diff --git a/nautilus_core/model/src/python/types/balance.rs b/nautilus_core/model/src/python/types/balance.rs index 26ed3b8bb93a..9f9337f843cc 100644 --- a/nautilus_core/model/src/python/types/balance.rs +++ b/nautilus_core/model/src/python/types/balance.rs @@ -73,11 +73,11 @@ impl AccountBalance { let free: f64 = free_str.parse::().unwrap(); let locked_str: &str = dict.get_item("locked")?.unwrap().extract()?; let locked: f64 = locked_str.parse::().unwrap(); - let currency = Currency::from_str(currency)?; + let currency = Currency::from_str(currency).map_err(to_pyvalue_err)?; let account_balance = Self::new( - Money::new(total, currency)?, - Money::new(locked, currency)?, - Money::new(free, currency)?, + Money::new(total, currency).map_err(to_pyvalue_err)?, + Money::new(locked, currency).map_err(to_pyvalue_err)?, + Money::new(free, currency).map_err(to_pyvalue_err)?, ) .unwrap(); Ok(account_balance) @@ -160,10 +160,10 @@ impl MarginBalance { let maintenance_str: &str = dict.get_item("maintenance")?.unwrap().extract()?; let maintenance: f64 = maintenance_str.parse::().unwrap(); let instrument_id_str: &str = dict.get_item("instrument_id")?.unwrap().extract()?; - let currency = Currency::from_str(currency)?; + let currency = Currency::from_str(currency).map_err(to_pyvalue_err)?; let account_balance = Self::new( - Money::new(initial, currency)?, - Money::new(maintenance, currency)?, + Money::new(initial, currency).map_err(to_pyvalue_err)?, + Money::new(maintenance, currency).map_err(to_pyvalue_err)?, InstrumentId::from_str(instrument_id_str).unwrap(), ) .unwrap(); From 198a14c5e35a0cc8cdc8edd1728a464b854b625d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 07:49:20 +1100 Subject: [PATCH 46/71] Minor refinements --- .../interactive_brokers/client/client.py | 3 ++- .../interactive_brokers/client/error.py | 11 ++++---- .../interactive_brokers/client/wrapper.py | 25 +++++++++++++++++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 28a1df14c945..cbe5dd7fa2e1 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -90,6 +90,7 @@ def __init__( component_name=f"{type(self).__name__}-{client_id:03d}", msgbus=msgbus, ) + # Config self._loop = loop self._cache = cache @@ -132,7 +133,7 @@ def __init__( # ConnectionMixin self._reconnect_attempts: int = 0 - self._max_reconnect_attempts: int | None = int(os.getenv("IB_MAX_RECONNECT_ATTEMPTS", 0)) + self._max_reconnect_attempts: int = int(os.getenv("IB_MAX_RECONNECT_ATTEMPTS", 0)) self._indefinite_reconnect: bool = False if self._max_reconnect_attempts else True self._reconnect_delay: int = 5 # seconds diff --git a/nautilus_trader/adapters/interactive_brokers/client/error.py b/nautilus_trader/adapters/interactive_brokers/client/error.py index bec85f64875f..5e780b24770e 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/error.py +++ b/nautilus_trader/adapters/interactive_brokers/client/error.py @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from inspect import iscoroutinefunction +from typing import Final from nautilus_trader.adapters.interactive_brokers.client.common import BaseMixin from nautilus_trader.common.enums import LogColor @@ -32,11 +33,11 @@ class InteractiveBrokersClientErrorMixin(BaseMixin): """ - WARNING_CODES = {1101, 1102, 110, 165, 202, 399, 404, 434, 492, 10167} - CLIENT_ERRORS = {502, 503, 504, 10038, 10182, 1100, 2110} - CONNECTIVITY_LOST_CODES = {1100, 1300, 2110} - CONNECTIVITY_RESTORED_CODES = {1101, 1102} - ORDER_REJECTION_CODES = {201, 203, 321, 10289, 10293} + WARNING_CODES: Final[set[int]] = {1101, 1102, 110, 165, 202, 399, 404, 434, 492, 10167} + CLIENT_ERRORS: Final[set[int]] = {502, 503, 504, 10038, 10182, 1100, 2110} + CONNECTIVITY_LOST_CODES: Final[set[int]] = {1100, 1300, 2110} + CONNECTIVITY_RESTORED_CODES: Final[set[int]] = {1101, 1102} + ORDER_REJECTION_CODES: Final[set[int]] = {201, 203, 321, 10289, 10293} def _log_message( self, diff --git a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py index 9e36c7d0f1f3..7e662c331dcd 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py +++ b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py @@ -1,3 +1,18 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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 typing import TYPE_CHECKING @@ -44,7 +59,7 @@ def __init__( self, nautilus_logger: Logger, client: "InteractiveBrokersClient", - ): + ) -> None: super().__init__() self._log = nautilus_logger self._client = client @@ -60,7 +75,13 @@ def logAnswer(self, fnName, fnParams): prms = fnParams self._log.debug(f"Msg handled: function={fnName} data={prms}") - def error(self, reqId: TickerId, errorCode: int, errorString: str, advancedOrderRejectJson=""): + def error( + self, + reqId: TickerId, + errorCode: int, + errorString: str, + advancedOrderRejectJson="", + ): """ Call this event in response to an error in communication or when TWS needs to send a message to the client. From 735eb57c16eb014335d39370823da095fbfafe1a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 08:16:25 +1100 Subject: [PATCH 47/71] Add Redis connection debug logging --- nautilus_core/common/src/redis.rs | 1 + nautilus_core/infrastructure/src/redis.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index 97aaa841a30b..9be0aed8fb7a 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -40,6 +40,7 @@ pub fn handle_messages_with_redis( ) -> anyhow::Result<()> { debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); let redis_url = get_redis_url(&config); + debug!("redis_url {redis_url}"); let default_timeout = 20; let timeout = get_timeout_duration(&config, default_timeout); let client = redis::Client::open(redis_url)?; diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 4ef644c3f63f..dc3db6ee5fe6 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -85,6 +85,7 @@ impl CacheDatabase for RedisCacheDatabase { ) -> anyhow::Result { debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); let redis_url = get_redis_url(&config); + debug!("redis_url {redis_url}"); let default_timeout = 20; let timeout = get_timeout_duration(&config, default_timeout); let client = redis::Client::open(redis_url)?; From 20af84d21f39467418610d0cf123a3a6d59c7d1e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 18:26:44 +1100 Subject: [PATCH 48/71] Add Rust correctness testing --- nautilus_core/core/src/correctness.rs | 141 ++++++++++++++++---------- 1 file changed, 89 insertions(+), 52 deletions(-) diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 72104791ecaa..2190c80cf7fa 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -156,7 +156,7 @@ mod tests { #[case("a a")] #[case(" a ")] #[case("abc")] - fn test_valid_string_with_valid_value(#[case] s: &str) { + fn test_check_valid_string_with_valid_value(#[case] s: &str) { assert!(check_valid_string(s, "value").is_ok()); } @@ -165,39 +165,49 @@ mod tests { #[case(" ")] // <-- whitespace-only #[case(" ")] // <-- whitespace-only string #[case("🦀")] // <-- contains non-ASCII char - fn test_valid_string_with_invalid_values(#[case] s: &str) { + fn test_check_valid_string_with_invalid_values(#[case] s: &str) { assert!(check_valid_string(s, "value").is_err()); } + #[rstest] + #[case(None)] + #[case(Some(" a"))] + #[case(Some("a "))] + #[case(Some("a a"))] + #[case(Some(" a "))] + #[case(Some("abc"))] + fn test_check_valid_string_optional_with_valid_value(#[case] s: Option<&str>) { + assert!(check_valid_string_optional(s, "value").is_ok()); + } + #[rstest] #[case("a", "a")] - fn test_string_contains_when_it_does_contain(#[case] s: &str, #[case] pat: &str) { + fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) { assert!(check_string_contains(s, pat, "value").is_ok()); } #[rstest] #[case("a", "b")] - fn test_string_contains_with_invalid_values(#[case] s: &str, #[case] pat: &str) { + fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) { assert!(check_string_contains(s, pat, "value").is_err()); } #[rstest] - #[case(0, 0, 0, "value")] - #[case(0, 0, 1, "value")] - #[case(1, 0, 1, "value")] - fn test_u8_in_range_inclusive_when_valid_values( - #[case] value: u8, - #[case] l: u8, - #[case] r: u8, - #[case] desc: &str, + #[case(0, 0, "left param", "right param")] + #[case(1, 1, "left param", "right param")] + fn test_check_equal_u8_when_equal( + #[case] lhs: u8, + #[case] rhs: u8, + #[case] lhs_param: &str, + #[case] rhs_param: &str, ) { - assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok()); + assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok()); } #[rstest] #[case(0, 1, "left param", "right param")] #[case(1, 0, "left param", "right param")] - fn test_u8_equal_when_invalid_values( + fn test_check_equal_u8_when_not_equal( #[case] lhs: u8, #[case] rhs: u8, #[case] lhs_param: &str, @@ -207,91 +217,118 @@ mod tests { } #[rstest] - #[case(0, 0, "left param", "right param")] - fn test_u8_equal_when_valid_values( - #[case] lhs: u8, - #[case] rhs: u8, - #[case] lhs_param: &str, - #[case] rhs_param: &str, + #[case(1, "value")] + fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) { + assert!(check_positive_u64(value, param).is_ok()); + } + + #[rstest] + #[case(0, "value")] + fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) { + assert!(check_positive_u64(value, param).is_err()); + } + + #[rstest] + #[case(1, "value")] + fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) { + assert!(check_positive_i64(value, param).is_ok()); + } + + #[rstest] + #[case(0, "value")] + #[case(-1, "value")] + fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) { + assert!(check_positive_i64(value, param).is_err()); + } + + #[rstest] + #[case(0.0, "value")] + #[case(1.0, "value")] + fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) { + assert!(check_non_negative_f64(value, param).is_ok()); + } + + #[rstest] + #[case(f64::NAN, "value")] + #[case(f64::INFINITY, "value")] + #[case(f64::NEG_INFINITY, "value")] + #[case(-0.1, "value")] + fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) { + assert!(check_non_negative_f64(value, param).is_err()); + } + + #[rstest] + #[case(0, 0, 0, "value")] + #[case(0, 0, 1, "value")] + #[case(1, 0, 1, "value")] + fn test_check_in_range_u8_inclusive_when_in_range( + #[case] value: u8, + #[case] l: u8, + #[case] r: u8, + #[case] desc: &str, ) { - assert!(check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok()); + assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_u8_in_range_inclusive_when_invalid_values( + fn test_check_in_range_u8_inclusive_when_out_of_range( #[case] value: u8, #[case] l: u8, #[case] r: u8, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_in_range_inclusive_u8(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u8(value, l, r, param).is_err()); } #[rstest] #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_u64_in_range_inclusive_when_valid_values( + fn test_check_in_range_u64_inclusive_when_in_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_in_range_inclusive_u64(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_u64_in_range_inclusive_when_invalid_values( + fn test_check_in_range_u64_inclusive_when_out_of_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_in_range_inclusive_u64(value, l, r, desc).is_err()); + assert!(check_in_range_inclusive_u64(value, l, r, param).is_err()); } #[rstest] #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_i64_in_range_inclusive_when_valid_values( + fn test_check_in_range_i64_inclusive_when_in_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_in_range_inclusive_i64(value, l, r, desc).is_ok()); + assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok()); } #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_i64_in_range_inclusive_when_invalid_values( + fn test_check_in_range_i64_inclusive_when_out_of_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, - #[case] desc: &str, + #[case] param: &str, ) { - assert!(check_in_range_inclusive_i64(value, l, r, desc).is_err()); - } - - #[rstest] - #[case(0.0, "value")] - #[case(1.0, "value")] - fn test_f64_non_negative_when_valid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_non_negative_f64(value, desc).is_ok()); - } - - #[rstest] - #[case(f64::NAN, "value")] - #[case(f64::INFINITY, "value")] - #[case(f64::NEG_INFINITY, "value")] - #[case(-0.1, "value")] - fn test_f64_non_negative_when_invalid_values(#[case] value: f64, #[case] desc: &str) { - assert!(check_non_negative_f64(value, desc).is_err()); + assert!(check_in_range_inclusive_i64(value, l, r, param).is_err()); } } From cef623fde8bd6e9746f84ec7eb5b9f856cceb472 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 18:31:19 +1100 Subject: [PATCH 49/71] Refine Rust correctness testing --- nautilus_core/core/src/correctness.rs | 45 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 2190c80cf7fa..7b19ee326cda 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -193,8 +193,8 @@ mod tests { } #[rstest] - #[case(0, 0, "left param", "right param")] - #[case(1, 1, "left param", "right param")] + #[case(0, 0, "left", "right")] + #[case(1, 1, "left", "right")] fn test_check_equal_u8_when_equal( #[case] lhs: u8, #[case] rhs: u8, @@ -205,8 +205,8 @@ mod tests { } #[rstest] - #[case(0, 1, "left param", "right param")] - #[case(1, 0, "left param", "right param")] + #[case(0, 1, "left", "right")] + #[case(1, 0, "left", "right")] fn test_check_equal_u8_when_not_equal( #[case] lhs: u8, #[case] rhs: u8, @@ -261,7 +261,7 @@ mod tests { #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_check_in_range_u8_inclusive_when_in_range( + fn test_check_in_range_inclusive_u8_when_in_range( #[case] value: u8, #[case] l: u8, #[case] r: u8, @@ -273,7 +273,7 @@ mod tests { #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_check_in_range_u8_inclusive_when_out_of_range( + fn test_check_in_range_inclusive_u8_when_out_of_range( #[case] value: u8, #[case] l: u8, #[case] r: u8, @@ -286,7 +286,7 @@ mod tests { #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_check_in_range_u64_inclusive_when_in_range( + fn test_check_in_range_inclusive_u64_when_in_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, @@ -298,7 +298,7 @@ mod tests { #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_check_in_range_u64_inclusive_when_out_of_range( + fn test_check_in_range_inclusive_u64_when_out_of_range( #[case] value: u64, #[case] l: u64, #[case] r: u64, @@ -311,7 +311,7 @@ mod tests { #[case(0, 0, 0, "value")] #[case(0, 0, 1, "value")] #[case(1, 0, 1, "value")] - fn test_check_in_range_i64_inclusive_when_in_range( + fn test_check_in_range_inclusive_i64_when_in_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, @@ -323,7 +323,7 @@ mod tests { #[rstest] #[case(0, 1, 2, "value")] #[case(3, 1, 2, "value")] - fn test_check_in_range_i64_inclusive_when_out_of_range( + fn test_check_in_range_inclusive_i64_when_out_of_range( #[case] value: i64, #[case] l: i64, #[case] r: i64, @@ -331,4 +331,29 @@ mod tests { ) { assert!(check_in_range_inclusive_i64(value, l, r, param).is_err()); } + + #[rstest] + #[case(0, 0, 0, "value")] + #[case(0, 0, 1, "value")] + #[case(1, 0, 1, "value")] + fn test_check_in_range_inclusive_usize_when_in_range( + #[case] value: usize, + #[case] l: usize, + #[case] r: usize, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok()); + } + + #[rstest] + #[case(0, 1, 2, "value")] + #[case(3, 1, 2, "value")] + fn test_check_in_range_inclusive_usize_when_out_of_range( + #[case] value: usize, + #[case] l: usize, + #[case] r: usize, + #[case] param: &str, + ) { + assert!(check_in_range_inclusive_usize(value, l, r, param).is_err()); + } } From b26adef115d1a528190236548731d5c055bf7f8e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 19:19:07 +1100 Subject: [PATCH 50/71] Refine Rust common Redis functions --- nautilus_core/common/src/redis.rs | 190 ++++++++++++++++------ nautilus_core/infrastructure/src/redis.rs | 22 +-- 2 files changed, 146 insertions(+), 66 deletions(-) diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index 9be0aed8fb7a..0164cb772851 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -38,14 +38,9 @@ pub fn handle_messages_with_redis( instance_id: UUID4, config: HashMap, ) -> anyhow::Result<()> { - debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); - let redis_url = get_redis_url(&config); - debug!("redis_url {redis_url}"); - let default_timeout = 20; - let timeout = get_timeout_duration(&config, default_timeout); - let client = redis::Client::open(redis_url)?; - let mut conn = client.get_connection_with_timeout(timeout)?; - debug!("Connected"); + let empty = Value::Object(serde_json::Map::new()); + let database_config = config.get("database").unwrap_or(&empty); + let mut conn = create_redis_connection(&database_config.clone())?; let stream_name = get_stream_name(trader_id, instance_id, &config); @@ -145,42 +140,59 @@ fn drain_buffer( pipe.query::<()>(conn).map_err(anyhow::Error::from) } -pub fn get_redis_url(config: &HashMap) -> String { - let empty = Value::Object(serde_json::Map::new()); - let database = config.get("database").unwrap_or(&empty); - - let host = database +pub fn get_redis_url(database_config: &serde_json::Value) -> String { + let host = database_config .get("host") - .map(|v| v.as_str().unwrap_or("127.0.0.1")); - let port = database.get("port").map(|v| v.as_u64().unwrap_or(6379)); - let username = database + .and_then(|v| v.as_str()) + .unwrap_or("127.0.0.1"); + let port = database_config + .get("port") + .and_then(|v| v.as_u64()) + .unwrap_or(6379); + let username = database_config .get("username") - .map(|v| v.as_str().unwrap_or_default()); - let password = database + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let password = database_config .get("password") - .map(|v| v.as_str().unwrap_or_default()); - let use_ssl = database.get("ssl").unwrap_or(&json!(false)); + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let use_ssl = database_config + .get("ssl") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let auth_part = if !username.is_empty() && !password.is_empty() { + format!("{}:{}@", username, password) + } else { + String::new() + }; format!( - "redis{}://{}:{}@{}:{}", - if use_ssl.as_bool().unwrap_or(false) { - "s" - } else { - "" - }, - username.unwrap_or(""), - password.unwrap_or(""), - host.unwrap(), - port.unwrap(), + "redis{}://{}{}:{}", + if use_ssl { "s" } else { "" }, + auth_part, + host, + port ) } -pub fn get_timeout_duration(config: &HashMap, default: u64) -> Duration { - let timeout_seconds = config - .get("database") - .and_then(|database| database.get("timeout").and_then(|v| v.as_u64())) - .unwrap_or(default); +pub fn create_redis_connection(database_config: &serde_json::Value) -> RedisResult { + let redis_url = get_redis_url(database_config); + debug!("Connecting to redis_url {redis_url}"); + let default_timeout = 20; + let timeout = get_timeout_duration(database_config, default_timeout); + let client = redis::Client::open(redis_url)?; + let conn = client.get_connection_with_timeout(timeout)?; + debug!("Connected"); + Ok(conn) +} +pub fn get_timeout_duration(database_config: &serde_json::Value, default: u64) -> Duration { + let timeout_seconds = database_config + .get("timeout") + .and_then(|v| v.as_u64()) + .unwrap_or(default); Duration::from_secs(timeout_seconds) } @@ -219,7 +231,6 @@ fn get_stream_name( .expect("Invalid configuration: `streams_prefix` is not a string"); stream_name.push_str(stream_prefix); stream_name.push(DELIMITER); - stream_name } @@ -232,6 +243,97 @@ mod tests { use super::*; + #[rstest] + fn test_get_redis_url_default_values() { + let config = json!({}); + let url = get_redis_url(&config); + assert_eq!(url, "redis://127.0.0.1:6379"); + } + + #[rstest] + fn test_get_redis_url_full_config_with_ssl() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "user", + "password": "pass", + "ssl": true, + }); + let url = get_redis_url(&config); + assert_eq!(url, "rediss://user:pass@example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_full_config_without_ssl() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "user", + "password": "pass", + "ssl": false, + }); + let url = get_redis_url(&config); + assert_eq!(url, "redis://user:pass@example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_missing_username_and_password() { + let config = json!({ + "host": "example.com", + "port": 6380, + "ssl": false, + }); + let url = get_redis_url(&config); + assert_eq!(url, "redis://example.com:6380"); + } + + #[rstest] + fn test_get_redis_url_ssl_default_false() { + let config = json!({ + "host": "example.com", + "port": 6380, + "username": "user", + "password": "pass", + // "ssl" is intentionally omitted to test default behavior + }); + let url = get_redis_url(&config); + assert_eq!(url, "redis://user:pass@example.com:6380"); + } + + #[rstest] + fn test_get_timeout_duration_default() { + let database_config = json!({}); + + let timeout_duration = get_timeout_duration(&database_config, 20); + assert_eq!(timeout_duration, Duration::from_secs(20)); + } + + #[rstest] + fn test_get_timeout_duration() { + let mut database_config = HashMap::new(); + database_config.insert("timeout".to_string(), json!(2)); + + let timeout_duration = get_timeout_duration(&json!(database_config), 20); + assert_eq!(timeout_duration, Duration::from_secs(2)); + } + + #[rstest] + fn test_get_buffer_interval_default() { + let config = HashMap::new(); + + let buffer_interval = get_buffer_interval(&config); + assert_eq!(buffer_interval, Duration::from_millis(0)); + } + + #[rstest] + fn test_get_buffer_interval() { + let mut config = HashMap::new(); + config.insert("buffer_interval_ms".to_string(), json!(100)); + + let buffer_interval = get_buffer_interval(&config); + assert_eq!(buffer_interval, Duration::from_millis(100)); + } + #[rstest] fn test_get_stream_name_with_trader_prefix_and_instance_id() { let trader_id = TraderId::from("tester-123"); @@ -259,20 +361,4 @@ mod tests { let key = get_stream_name(trader_id, instance_id, &config); assert_eq!(key, format!("streams:")); } - - #[rstest] - fn test_get_buffer_interval_default() { - let config = HashMap::new(); - let buffer_interval = get_buffer_interval(&config); - assert_eq!(buffer_interval, Duration::from_millis(0)); - } - - #[rstest] - fn test_get_buffer_interval() { - let mut config = HashMap::new(); - config.insert("buffer_interval_ms".to_string(), json!(100)); - - let buffer_interval = get_buffer_interval(&config); - assert_eq!(buffer_interval, Duration::from_millis(100)); - } } diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index dc3db6ee5fe6..8612b73073c2 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -22,13 +22,12 @@ use std::{ use nautilus_common::{ cache::{CacheDatabase, DatabaseCommand, DatabaseOperation}, - redis::{get_buffer_interval, get_redis_url, get_timeout_duration}, + redis::{create_redis_connection, get_buffer_interval}, }; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::{Commands, Connection, Pipeline}; -use serde_json::json; -use tracing::debug; +use serde_json::{json, Value}; // Error constants const CHANNEL_TX_FAILED: &str = "Failed to send to channel"; @@ -83,14 +82,9 @@ impl CacheDatabase for RedisCacheDatabase { instance_id: UUID4, config: HashMap, ) -> anyhow::Result { - debug!("Initializing trader_id={trader_id}, instance_id={instance_id}, config={config:?}"); - let redis_url = get_redis_url(&config); - debug!("redis_url {redis_url}"); - let default_timeout = 20; - let timeout = get_timeout_duration(&config, default_timeout); - let client = redis::Client::open(redis_url)?; - let conn = client.get_connection_with_timeout(timeout)?; - debug!("Connected"); + let empty = Value::Object(serde_json::Map::new()); + let database_config = config.get("database").unwrap_or(&empty); + let conn = create_redis_connection(&database_config.clone())?; let (tx, rx) = channel::(); let trader_key = get_trader_key(trader_id, instance_id, &config); @@ -173,9 +167,9 @@ impl CacheDatabase for RedisCacheDatabase { trader_key: String, config: HashMap, ) { - let redis_url = get_redis_url(&config); - let client = redis::Client::open(redis_url).unwrap(); - let mut conn = client.get_connection().unwrap(); + let empty = Value::Object(serde_json::Map::new()); + let database_config = config.get("database").unwrap_or(&empty); + let mut conn = create_redis_connection(&database_config.clone()).unwrap(); // Buffering let mut buffer: VecDeque = VecDeque::new(); From 42831847f99f8233ed94e1dab733409fbe7e963c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Mar 2024 20:22:44 +1100 Subject: [PATCH 51/71] Improve Redis connection logging --- nautilus_core/common/src/redis.rs | 8 +++++--- nautilus_core/infrastructure/src/redis.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index 0164cb772851..d4c77e911409 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -38,8 +38,10 @@ pub fn handle_messages_with_redis( instance_id: UUID4, config: HashMap, ) -> anyhow::Result<()> { - let empty = Value::Object(serde_json::Map::new()); - let database_config = config.get("database").unwrap_or(&empty); + let database_config = config + .get("database") + .ok_or(anyhow::anyhow!("No database config"))?; + debug!("Creating msgbus redis connection"); let mut conn = create_redis_connection(&database_config.clone())?; let stream_name = get_stream_name(trader_id, instance_id, &config); @@ -179,7 +181,7 @@ pub fn get_redis_url(database_config: &serde_json::Value) -> String { pub fn create_redis_connection(database_config: &serde_json::Value) -> RedisResult { let redis_url = get_redis_url(database_config); - debug!("Connecting to redis_url {redis_url}"); + debug!("Connecting to {redis_url}"); let default_timeout = 20; let timeout = get_timeout_duration(database_config, default_timeout); let client = redis::Client::open(redis_url)?; diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 8612b73073c2..662e7c177be2 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -28,6 +28,7 @@ use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::trader_id::TraderId; use redis::{Commands, Connection, Pipeline}; use serde_json::{json, Value}; +use tracing::debug; // Error constants const CHANNEL_TX_FAILED: &str = "Failed to send to channel"; @@ -82,8 +83,10 @@ impl CacheDatabase for RedisCacheDatabase { instance_id: UUID4, config: HashMap, ) -> anyhow::Result { - let empty = Value::Object(serde_json::Map::new()); - let database_config = config.get("database").unwrap_or(&empty); + let database_config = config + .get("database") + .ok_or(anyhow::anyhow!("No database config"))?; + debug!("Creating cache read redis connection"); let conn = create_redis_connection(&database_config.clone())?; let (tx, rx) = channel::(); @@ -169,6 +172,7 @@ impl CacheDatabase for RedisCacheDatabase { ) { let empty = Value::Object(serde_json::Map::new()); let database_config = config.get("database").unwrap_or(&empty); + debug!("Creating cache write redis connection"); let mut conn = create_redis_connection(&database_config.clone()).unwrap(); // Buffering From e35bb9075de092c8241df8cae556d976628110da Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Mar 2024 16:15:00 +1100 Subject: [PATCH 52/71] Improve Redis connection logging --- nautilus_core/infrastructure/src/redis.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index 662e7c177be2..d421ad8c99a8 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -86,7 +86,7 @@ impl CacheDatabase for RedisCacheDatabase { let database_config = config .get("database") .ok_or(anyhow::anyhow!("No database config"))?; - debug!("Creating cache read redis connection"); + debug!("Creating cache-read redis connection"); let conn = create_redis_connection(&database_config.clone())?; let (tx, rx) = channel::(); @@ -172,7 +172,7 @@ impl CacheDatabase for RedisCacheDatabase { ) { let empty = Value::Object(serde_json::Map::new()); let database_config = config.get("database").unwrap_or(&empty); - debug!("Creating cache write redis connection"); + debug!("Creating cache-write redis connection"); let mut conn = create_redis_connection(&database_config.clone()).unwrap(); // Buffering From b7d9a9a2016c27fbea79e7906d907dc152a1440d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Mar 2024 18:34:19 +1100 Subject: [PATCH 53/71] Improve efficiency of Redis queries --- nautilus_core/infrastructure/src/redis.rs | 2 ++ nautilus_trader/cache/database.pxd | 27 --------------- nautilus_trader/cache/database.pyx | 42 ++++------------------- 3 files changed, 9 insertions(+), 62 deletions(-) diff --git a/nautilus_core/infrastructure/src/redis.rs b/nautilus_core/infrastructure/src/redis.rs index d421ad8c99a8..a3b7471450bf 100644 --- a/nautilus_core/infrastructure/src/redis.rs +++ b/nautilus_core/infrastructure/src/redis.rs @@ -116,6 +116,8 @@ impl CacheDatabase for RedisCacheDatabase { } fn keys(&mut self, pattern: &str) -> anyhow::Result> { + let pattern = format!("{}{DELIMITER}{}", self.trader_key, pattern); + debug!("Querying keys: {pattern}"); match self.conn.keys(pattern) { Ok(keys) => Ok(keys), Err(e) => Err(e.into()), diff --git a/nautilus_trader/cache/database.pxd b/nautilus_trader/cache/database.pxd index 52a6a2955ff5..21cd9afbf938 100644 --- a/nautilus_trader/cache/database.pxd +++ b/nautilus_trader/cache/database.pxd @@ -18,32 +18,5 @@ from nautilus_trader.serialization.base cimport Serializer cdef class CacheDatabaseAdapter(CacheDatabaseFacade): - cdef str _key_trader - cdef str _key_general - cdef str _key_currencies - cdef str _key_instruments - cdef str _key_synthetics - cdef str _key_accounts - cdef str _key_orders - cdef str _key_positions - cdef str _key_actors - cdef str _key_strategies - - cdef str _key_index_order_ids - cdef str _key_index_order_position - cdef str _key_index_order_client - cdef str _key_index_orders - cdef str _key_index_orders_open - cdef str _key_index_orders_closed - cdef str _key_index_orders_emulated - cdef str _key_index_orders_inflight - cdef str _key_index_positions - cdef str _key_index_positions_open - cdef str _key_index_positions_closed - - cdef str _key_snapshots_orders - cdef str _key_snapshots_positions - cdef str _key_heartbeat - cdef Serializer _serializer cdef object _backing diff --git a/nautilus_trader/cache/database.pyx b/nautilus_trader/cache/database.pyx index 696fe39602c1..bed0e6c0cad5 100644 --- a/nautilus_trader/cache/database.pyx +++ b/nautilus_trader/cache/database.pyx @@ -154,34 +154,6 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): self._log.info(f"{config.use_trader_prefix=}", LogColor.BLUE) self._log.info(f"{config.use_instance_id=}", LogColor.BLUE) - # Database keys - self._key_trader = f"{_TRADER}-{trader_id}" # noqa - self._key_general = f"{self._key_trader}:{_GENERAL}:" # noqa - self._key_currencies = f"{self._key_trader}:{_CURRENCIES}:" # noqa - self._key_instruments = f"{self._key_trader}:{_INSTRUMENTS}:" # noqa - self._key_synthetics = f"{self._key_trader}:{_SYNTHETICS}:" # noqa - self._key_accounts = f"{self._key_trader}:{_ACCOUNTS}:" # noqa - self._key_orders = f"{self._key_trader}:{_ORDERS}:" # noqa - self._key_positions = f"{self._key_trader}:{_POSITIONS}:" # noqa - self._key_actors = f"{self._key_trader}:{_ACTORS}:" # noqa - self._key_strategies = f"{self._key_trader}:{_STRATEGIES}:" # noqa - - self._key_index_order_ids = f"{self._key_trader}:{_INDEX_ORDER_IDS}:" - self._key_index_order_position = f"{self._key_trader}:{_INDEX_ORDER_POSITION}:" - self._key_index_order_client = f"{self._key_trader}:{_INDEX_ORDER_CLIENT}:" - self._key_index_orders = f"{self._key_trader}:{_INDEX_ORDERS}" - self._key_index_orders_open = f"{self._key_trader}:{_INDEX_ORDERS_OPEN}" - self._key_index_orders_closed = f"{self._key_trader}:{_INDEX_ORDERS_CLOSED}" - self._key_index_orders_emulated = f"{self._key_trader}:{_INDEX_ORDERS_EMULATED}" - self._key_index_orders_inflight = f"{self._key_trader}:{_INDEX_ORDERS_INFLIGHT}" - self._key_index_positions = f"{self._key_trader}:{_INDEX_POSITIONS}" - self._key_index_positions_open = f"{self._key_trader}:{_INDEX_POSITIONS_OPEN}" - self._key_index_positions_closed = f"{self._key_trader}:{_INDEX_POSITIONS_CLOSED}" - - self._key_snapshots_orders = f"{self._key_trader}:{_SNAPSHOTS_ORDERS}:" - self._key_snapshots_positions = f"{self._key_trader}:{_SNAPSHOTS_POSITIONS}:" - self._key_heartbeat = f"{self._key_trader}:{_HEARTBEAT}" - self._serializer = serializer self._backing = nautilus_pyo3.RedisCacheDatabase( @@ -242,7 +214,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict general = {} - cdef list general_keys = self._backing.keys(f"*:{_GENERAL}:*") + cdef list general_keys = self._backing.keys(f"{_GENERAL}:*") if not general_keys: return general @@ -271,7 +243,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict currencies = {} - cdef list currency_keys = self._backing.keys(f"*:{_CURRENCIES}*") + cdef list currency_keys = self._backing.keys(f"{_CURRENCIES}*") if not currency_keys: return currencies @@ -299,7 +271,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict instruments = {} - cdef list instrument_keys = self._backing.keys(f"*:{_INSTRUMENTS}*") + cdef list instrument_keys = self._backing.keys(f"{_INSTRUMENTS}*") if not instrument_keys: return instruments @@ -327,7 +299,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict synthetics = {} - cdef list synthetic_keys = self._backing.keys(f"*:{_SYNTHETICS}*") + cdef list synthetic_keys = self._backing.keys(f"{_SYNTHETICS}*") if not synthetic_keys: return synthetics @@ -355,7 +327,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict accounts = {} - cdef list account_keys = self._backing.keys(f"*:{_ACCOUNTS}*") + cdef list account_keys = self._backing.keys(f"{_ACCOUNTS}*") if not account_keys: return accounts @@ -384,7 +356,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict orders = {} - cdef list order_keys = self._backing.keys(f"*:{_ORDERS}*") + cdef list order_keys = self._backing.keys(f"{_ORDERS}*") if not order_keys: return orders @@ -412,7 +384,7 @@ cdef class CacheDatabaseAdapter(CacheDatabaseFacade): """ cdef dict positions = {} - cdef list position_keys = self._backing.keys(f"*:{_POSITIONS}*") + cdef list position_keys = self._backing.keys(f"{_POSITIONS}*") if not position_keys: return positions From ead16ae9a4bf85961c2a9e91f6d25d6c34812f10 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Mar 2024 19:15:43 +1100 Subject: [PATCH 54/71] Update dependencies --- nautilus_core/Cargo.lock | 27 +++++++------- nautilus_core/Cargo.toml | 11 ++++-- poetry.lock | 76 ++++++++++++++++++++-------------------- pyproject.toml | 2 +- 4 files changed, 62 insertions(+), 54 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 53cc2ad6cf1b..bbc6b5244017 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -309,7 +309,7 @@ version = "50.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ff3e9c01f7cd169379d269f926892d0e622a704960350d09d331be3ec9e0029" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", ] [[package]] @@ -496,9 +496,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] @@ -2877,7 +2877,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -3246,7 +3246,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "chrono", "flate2", "hex", @@ -3261,7 +3261,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "chrono", "hex", ] @@ -3489,6 +3489,7 @@ dependencies = [ "tokio-rustls", "tokio-util", "url", + "webpki-roots", ] [[package]] @@ -3763,7 +3764,7 @@ version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -4240,7 +4241,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "bytes", "crc", @@ -4282,7 +4283,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64", - "bitflags 2.4.2", + "bitflags 2.5.0", "byteorder", "crc", "dotenvy", @@ -5058,9 +5059,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", ] @@ -5073,9 +5074,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" +checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" [[package]] name = "vcpkg" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 2e9f2fc8b5ea..c919535e9ab4 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -35,7 +35,14 @@ log = { version = "0.4.21", features = ["std", "kv_unstable", "serde", "release_ pyo3 = { version = "0.20.3", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" -redis = { version = "0.25.2", features = ["tokio-comp", "tls-rustls", "tokio-rustls-comp", "keep-alive", "connection-manager"] } +redis = { version = "0.25.2", features = [ + "connection-manager", + "keep-alive", + "tls-rustls", + "tls-rustls-webpki-roots", + "tokio-comp", + "tokio-rustls-comp", +] } rmp-serde = "1.1.2" rust_decimal = "1.34.3" rust_decimal_macros = "1.34.2" @@ -47,7 +54,7 @@ thousands = "0.2.0" tracing = "0.1.40" tokio = { version = "1.36.0", features = ["full"] } ustr = { version = "1.0.0", features = ["serde"] } -uuid = { version = "1.7.0", features = ["v4"] } +uuid = { version = "1.8.0", features = ["v4"] } # dev-dependencies criterion = "0.5.1" diff --git a/poetry.lock b/poetry.lock index 13800f280a20..22a2d174c97f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1600,47 +1600,47 @@ files = [ [[package]] name = "pyarrow" -version = "15.0.1" +version = "15.0.2" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-15.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c2ddb3be5ea938c329a84171694fc230b241ce1b6b0ff1a0280509af51c375fa"}, - {file = "pyarrow-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7543ea88a0ff72f8e6baaf9bfdbec2c62aeabdbede9e4a571c71cc3bc43b6302"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1519e218a6941fc074e4501088d891afcb2adf77c236e03c34babcf3d6a0d1c7"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cafa86e1944761970d3b3fc0411b14ff9b5c2b73cd22aaf470d7a3976335f5"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be5c3d463e33d03eab496e1af7916b1d44001c08f0f458ad27dc16093a020638"}, - {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:47b1eda15d3aa3f49a07b1808648e1397e5dc6a80a30bf87faa8e2d02dad7ac3"}, - {file = "pyarrow-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e524a31be7db22deebbbcf242b189063ab9a7652c62471d296b31bc6e3cae77b"}, - {file = "pyarrow-15.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a476fefe8bdd56122fb0d4881b785413e025858803cc1302d0d788d3522b374d"}, - {file = "pyarrow-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:309e6191be385f2e220586bfdb643f9bb21d7e1bc6dd0a6963dc538e347b2431"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83bc586903dbeb4365cbc72b602f99f70b96c5882e5dfac5278813c7d624ca3c"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e652daac6d8b05280cd2af31c0fb61a4490ec6a53dc01588014d9fa3fdbee9"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:abad2e08652df153a72177ce20c897d083b0c4ebeec051239e2654ddf4d3c996"}, - {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cde663352bc83ad75ba7b3206e049ca1a69809223942362a8649e37bd22f9e3b"}, - {file = "pyarrow-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1b6e237dd7a08482a8b8f3f6512d258d2460f182931832a8c6ef3953203d31e1"}, - {file = "pyarrow-15.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd167536ee23192760b8c731d39b7cfd37914c27fd4582335ffd08450ff799d"}, - {file = "pyarrow-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c08bb31eb2984ba5c3747d375bb522e7e536b8b25b149c9cb5e1c49b0ccb736"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f9c1d630ed2524bd1ddf28ec92780a7b599fd54704cd653519f7ff5aec177a"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5186048493395220550bca7b524420471aac2d77af831f584ce132680f55c3df"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31dc30c7ec8958da3a3d9f31d6c3630429b2091ede0ecd0d989fd6bec129f0e4"}, - {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3f111a014fb8ac2297b43a74bf4495cc479a332908f7ee49cb7cbd50714cb0c1"}, - {file = "pyarrow-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a6d1f7c15d7f68f08490d0cb34611497c74285b8a6bbeab4ef3fc20117310983"}, - {file = "pyarrow-15.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9ad931b996f51c2f978ed517b55cb3c6078272fb4ec579e3da5a8c14873b698d"}, - {file = "pyarrow-15.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:738f6b53ab1c2f66b2bde8a1d77e186aeaab702d849e0dfa1158c9e2c030add3"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c1c3fc16bc74e33bf8f1e5a212938ed8d88e902f372c4dac6b5bad328567d2f"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fa92512128f6c1b8dde0468c1454dd70f3bff623970e370d52efd4d24fd0be"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b4157f307c202cbbdac147d9b07447a281fa8e63494f7fc85081da351ec6ace9"}, - {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b75e7da26f383787f80ad76143b44844ffa28648fcc7099a83df1538c078d2f2"}, - {file = "pyarrow-15.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a99eac76ae14096c209850935057b9e8ce97a78397c5cde8724674774f34e5d"}, - {file = "pyarrow-15.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:dd532d3177e031e9b2d2df19fd003d0cc0520d1747659fcabbd4d9bb87de508c"}, - {file = "pyarrow-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce8c89848fd37e5313fc2ce601483038ee5566db96ba0808d5883b2e2e55dc53"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862eac5e5f3b6477f7a92b2f27e560e1f4e5e9edfca9ea9da8a7478bb4abd5ce"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0ea3a29cd5cb99bf14c1c4533eceaa00ea8fb580950fb5a89a5c771a994a4e"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bb902f780cfd624b2e8fd8501fadab17618fdb548532620ef3d91312aaf0888a"}, - {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4f87757f02735a6bb4ad2e1b98279ac45d53b748d5baf52401516413007c6999"}, - {file = "pyarrow-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:efd3816c7fbfcbd406ac0f69873cebb052effd7cdc153ae5836d1b00845845d7"}, - {file = "pyarrow-15.0.1.tar.gz", hash = "sha256:21d812548d39d490e0c6928a7c663f37b96bf764034123d4b4ab4530ecc757a9"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:88b340f0a1d05b5ccc3d2d986279045655b1fe8e41aba6ca44ea28da0d1455d8"}, + {file = "pyarrow-15.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eaa8f96cecf32da508e6c7f69bb8401f03745c050c1dd42ec2596f2e98deecac"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c6753ed4f6adb8461e7c383e418391b8d8453c5d67e17f416c3a5d5709afbd"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f639c059035011db8c0497e541a8a45d98a58dbe34dc8fadd0ef128f2cee46e5"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:290e36a59a0993e9a5224ed2fb3e53375770f07379a0ea03ee2fce2e6d30b423"}, + {file = "pyarrow-15.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:06c2bb2a98bc792f040bef31ad3e9be6a63d0cb39189227c08a7d955db96816e"}, + {file = "pyarrow-15.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:f7a197f3670606a960ddc12adbe8075cea5f707ad7bf0dffa09637fdbb89f76c"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5f8bc839ea36b1f99984c78e06e7a06054693dc2af8920f6fb416b5bca9944e4"}, + {file = "pyarrow-15.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5e81dfb4e519baa6b4c80410421528c214427e77ca0ea9461eb4097c328fa33"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4f240852b302a7af4646c8bfe9950c4691a419847001178662a98915fd7ee7"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e7d9cfb5a1e648e172428c7a42b744610956f3b70f524aa3a6c02a448ba853e"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2d4f905209de70c0eb5b2de6763104d5a9a37430f137678edfb9a675bac9cd98"}, + {file = "pyarrow-15.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:90adb99e8ce5f36fbecbbc422e7dcbcbed07d985eed6062e459e23f9e71fd197"}, + {file = "pyarrow-15.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:b116e7fd7889294cbd24eb90cd9bdd3850be3738d61297855a71ac3b8124ee38"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:25335e6f1f07fdaa026a61c758ee7d19ce824a866b27bba744348fa73bb5a440"}, + {file = "pyarrow-15.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90f19e976d9c3d8e73c80be84ddbe2f830b6304e4c576349d9360e335cd627fc"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a22366249bf5fd40ddacc4f03cd3160f2d7c247692945afb1899bab8a140ddfb"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2a335198f886b07e4b5ea16d08ee06557e07db54a8400cc0d03c7f6a22f785f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e6d459c0c22f0b9c810a3917a1de3ee704b021a5fb8b3bacf968eece6df098f"}, + {file = "pyarrow-15.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:033b7cad32198754d93465dcfb71d0ba7cb7cd5c9afd7052cab7214676eec38b"}, + {file = "pyarrow-15.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:29850d050379d6e8b5a693098f4de7fd6a2bea4365bfd073d7c57c57b95041ee"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:7167107d7fb6dcadb375b4b691b7e316f4368f39f6f45405a05535d7ad5e5058"}, + {file = "pyarrow-15.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e85241b44cc3d365ef950432a1b3bd44ac54626f37b2e3a0cc89c20e45dfd8bf"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:248723e4ed3255fcd73edcecc209744d58a9ca852e4cf3d2577811b6d4b59818"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ff3bdfe6f1b81ca5b73b70a8d482d37a766433823e0c21e22d1d7dde76ca33f"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:f3d77463dee7e9f284ef42d341689b459a63ff2e75cee2b9302058d0d98fe142"}, + {file = "pyarrow-15.0.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:8c1faf2482fb89766e79745670cbca04e7018497d85be9242d5350cba21357e1"}, + {file = "pyarrow-15.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:28f3016958a8e45a1069303a4a4f6a7d4910643fc08adb1e2e4a7ff056272ad3"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:89722cb64286ab3d4daf168386f6968c126057b8c7ec3ef96302e81d8cdb8ae4"}, + {file = "pyarrow-15.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cd0ba387705044b3ac77b1b317165c0498299b08261d8122c96051024f953cd5"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2459bf1f22b6a5cdcc27ebfd99307d5526b62d217b984b9f5c974651398832"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58922e4bfece8b02abf7159f1f53a8f4d9f8e08f2d988109126c17c3bb261f22"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:adccc81d3dc0478ea0b498807b39a8d41628fa9210729b2f718b78cb997c7c91"}, + {file = "pyarrow-15.0.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:8bd2baa5fe531571847983f36a30ddbf65261ef23e496862ece83bdceb70420d"}, + {file = "pyarrow-15.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6669799a1d4ca9da9c7e06ef48368320f5856f36f9a4dd31a11839dda3f6cc8c"}, + {file = "pyarrow-15.0.2.tar.gz", hash = "sha256:9c9bc803cb3b7bfacc1e96ffbfd923601065d9d3f911179d81e72d99fd74a3d9"}, ] [package.dependencies] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "9c4de1ebaa97361596d80ae81d9bcbd672f01914bbb8962d970fa72ad7aa8d2a" +content-hash = "a9178495b3efb2556925ccdd78e19f202495df9caebf254bcc5d336c7e424b6e" diff --git a/pyproject.toml b/pyproject.toml index 789329e855dd..da3bb8ac337f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ click = "^8.1.7" fsspec = "==2023.6.0" # Pinned due breaking changes msgspec = "^0.18.6" pandas = "^2.2.1" -pyarrow = ">=15.0.1" +pyarrow = ">=15.0.2" pytz = ">=2023.4.0" tqdm = "^4.66.2" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} From fa0c098f03507bee0ef2c357f74611215d3385d8 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Tue, 19 Mar 2024 10:25:35 +0100 Subject: [PATCH 55/71] Implement LimitOrder in Rust and add pyo3 bindings (#1550) --- nautilus_core/model/src/orders/base.rs | 14 +- nautilus_core/model/src/orders/limit.rs | 130 +++- nautilus_core/model/src/orders/stubs.rs | 4 +- .../model/src/python/events/account/state.rs | 1 - .../model/src/python/orders/limit.rs | 641 ++++++++++++++++++ nautilus_core/model/src/python/orders/mod.rs | 1 + nautilus_trader/core/nautilus_pyo3.pyi | 82 ++- nautilus_trader/model/orders/limit.pxd | 3 + nautilus_trader/model/orders/limit.pyx | 41 +- nautilus_trader/test_kit/rust/orders_pyo3.py | 45 +- .../unit_tests/model/instruments/__init__.py | 14 + tests/unit_tests/model/objects/__init__.py | 14 + tests/unit_tests/model/orders/__init__.py | 14 + .../model/orders/test_limit_order_pyo3.py | 82 +++ tests/unit_tests/model/test_orders.py | 4 +- 15 files changed, 1072 insertions(+), 18 deletions(-) create mode 100644 nautilus_core/model/src/python/orders/limit.rs create mode 100644 tests/unit_tests/model/orders/__init__.py create mode 100644 tests/unit_tests/model/orders/test_limit_order_pyo3.py diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 314853d0e063..1b3a328418a8 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -500,6 +500,10 @@ pub trait Order { fn is_pending_cancel(&self) -> bool { self.status() == OrderStatus::PendingCancel } + fn is_spawned(&self) -> bool { + self.exec_algorithm_id().is_some() + && self.exec_spawn_id().unwrap() != self.client_order_id() + } } impl From<&T> for OrderInitialized @@ -631,11 +635,11 @@ impl OrderCore { order_type, quantity, time_in_force, - liquidity_side: None, + liquidity_side: Some(LiquiditySide::NoLiquiditySide), is_reduce_only: reduce_only, is_quote_quantity: quote_quantity, - emulation_trigger, - contingency_type, + emulation_trigger: emulation_trigger.or(Some(TriggerType::NoTrigger)), + contingency_type: contingency_type.or(Some(ContingencyType::NoContingency)), order_list_id, linked_order_ids, parent_order_id, @@ -838,6 +842,10 @@ impl OrderCore { pub fn commissions(&self) -> HashMap { self.commissions.clone() } + + pub fn init_event(&self) -> Option<&OrderEvent> { + self.events.first() + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index b4d68afe1573..8eebacdfc8aa 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -15,10 +15,13 @@ use std::{ collections::HashMap, + fmt::Display, ops::{Deref, DerefMut}, }; +use anyhow::bail; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use serde::{Deserialize, Serialize}; use ustr::Ustr; use super::base::{Order, OrderCore}; @@ -35,10 +38,13 @@ use crate::{ venue::Venue, venue_order_id::VenueOrderId, }, orders::base::OrderError, - types::{price::Price, quantity::Quantity}, + types::{ + price::Price, + quantity::{check_quantity_positive, Quantity}, + }, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -81,6 +87,17 @@ impl LimitOrder { init_id: UUID4, ts_init: UnixNanos, ) -> anyhow::Result { + check_quantity_positive(quantity)?; + if time_in_force == TimeInForce::Gtd { + if expire_time.is_none() { + bail!("Condition failed: `expire_time` is required for `GTD` order") + } + if let Some(time) = expire_time { + if time == 0 { + bail!("`expire_time` for `GTD` Limit order should be higher then 0") + } + } + } Ok(Self { core: OrderCore::new( trader_id, @@ -106,7 +123,7 @@ impl LimitOrder { ts_init, ), price, - expire_time, + expire_time: expire_time.or(Some(0)), is_post_only: post_only, display_qty, trigger_instrument_id, @@ -128,6 +145,12 @@ impl DerefMut for LimitOrder { } } +impl PartialEq for LimitOrder { + fn eq(&self, other: &Self) -> bool { + self.client_order_id == other.client_order_id + } +} + impl Order for LimitOrder { fn status(&self) -> OrderStatus { self.status @@ -379,6 +402,105 @@ impl From for LimitOrder { event.event_id, event.ts_event, ) - .unwrap() // SAFETY: From can panic + .unwrap() + } +} + +impl Display for LimitOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "LimitOrder(\ + {} {} {} {} @ {} {}, \ + status={}, \ + client_order_id={}, \ + venue_order_id={}, \ + position_id={}, \ + exec_algorithm_id={}, \ + exec_spawn_id={}, \ + tags={:?}\ + )", + self.side, + self.quantity.to_formatted_string(), + self.instrument_id, + self.order_type, + self.price, + self.time_in_force, + self.status, + self.client_order_id, + self.venue_order_id.map_or_else( + || "None".to_string(), + |venue_order_id| format!("{venue_order_id}") + ), + self.position_id.map_or_else( + || "None".to_string(), + |position_id| format!("{position_id}") + ), + self.exec_algorithm_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.exec_spawn_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.tags + ) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use crate::{ + enums::{OrderSide, TimeInForce}, + instruments::{currency_pair::CurrencyPair, stubs::*}, + orders::stubs::TestOrderStubs, + types::{price::Price, quantity::Quantity}, + }; + + #[rstest] + fn test_display(audusd_sim: CurrencyPair) { + let order = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("1.00000"), + Quantity::from(100_000), + None, + None, + ); + assert_eq!( + order.to_string(), + "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, \ + status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, \ + venue_order_id=None, position_id=None, exec_algorithm_id=None, \ + exec_spawn_id=O-19700101-0000-000-001-1, tags=None)" + ); + } + + #[rstest] + #[should_panic(expected = "Condition failed: invalid `Quantity`, should be positive and was 0")] + fn test_positive_quantity_condition(audusd_sim: CurrencyPair) { + let _ = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("0.8"), + Quantity::from(0), + None, + None, + ); + } + + #[rstest] + #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")] + fn test_correct_expiration_with_time_in_force_gtd(audusd_sim: CurrencyPair) { + let _ = TestOrderStubs::limit_order( + audusd_sim.id, + OrderSide::Buy, + Price::from("0.8"), + Quantity::from(1), + None, + Some(TimeInForce::Gtd), + ); } } diff --git a/nautilus_core/model/src/orders/stubs.rs b/nautilus_core/model/src/orders/stubs.rs index 7d623bacf790..250963637a1e 100644 --- a/nautilus_core/model/src/orders/stubs.rs +++ b/nautilus_core/model/src/orders/stubs.rs @@ -149,7 +149,7 @@ impl TestOrderStubs { let trader = trader_id(); let strategy = strategy_id_ema_cross(); let client_order_id = - client_order_id.unwrap_or(ClientOrderId::from("O-19700101-010000-001-001-1")); + client_order_id.unwrap_or(ClientOrderId::from("O-19700101-0000-000-001-1")); let time_in_force = time_in_force.unwrap_or(TimeInForce::Gtc); LimitOrder::new( trader, @@ -173,7 +173,7 @@ impl TestOrderStubs { None, None, None, - None, + Some(client_order_id), None, UUID4::new(), 12_321_312_321_312, diff --git a/nautilus_core/model/src/python/events/account/state.rs b/nautilus_core/model/src/python/events/account/state.rs index 0216dd64e60f..618238c9b575 100644 --- a/nautilus_core/model/src/python/events/account/state.rs +++ b/nautilus_core/model/src/python/events/account/state.rs @@ -124,7 +124,6 @@ impl AccountState { #[staticmethod] #[pyo3(name = "from_dict")] pub fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { - // from_dict_pyo3(py, values) let dict = values.as_ref(py); let account_id: &str = dict.get_item("account_id")?.unwrap().extract()?; let account_type: &str = dict.get_item("account_type")?.unwrap().extract::<&str>()?; diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs new file mode 100644 index 000000000000..00a4f61b4b2e --- /dev/null +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -0,0 +1,641 @@ +// ------------------------------------------------------------------------------------------------- +// 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::collections::HashMap; + +use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use pyo3::{ + basic::CompareOp, + prelude::*, + types::{PyDict, PyList}, +}; +use ustr::Ustr; + +use crate::{ + enums::{ + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + }, + identifiers::{ + client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, + instrument_id::InstrumentId, order_list_id::OrderListId, strategy_id::StrategyId, + trader_id::TraderId, + }, + orders::{ + base::{str_hashmap_to_ustr, Order}, + limit::LimitOrder, + }, + types::{price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl LimitOrder { + #[new] + #[allow(clippy::too_many_arguments)] + fn py_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + time_in_force: TimeInForce, + post_only: bool, + reduce_only: bool, + quote_quantity: bool, + init_id: UUID4, + ts_init: UnixNanos, + expire_time: Option, + display_qty: Option, + emulation_trigger: Option, + trigger_instrument_id: Option, + contingency_type: Option, + order_list_id: Option, + linked_order_ids: Option>, + parent_order_id: Option, + exec_algorithm_id: Option, + exec_algorithm_params: Option>, + exec_spawn_id: Option, + tags: Option, + ) -> PyResult { + let exec_algorithm_params = exec_algorithm_params.map(str_hashmap_to_ustr); + Ok(Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + price, + time_in_force, + expire_time, + post_only, + reduce_only, + quote_quantity, + display_qty, + emulation_trigger, + trigger_instrument_id, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags.map(|s| Ustr::from(&s)), + init_id, + ts_init, + ) + .unwrap()) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + self.to_string() + } + + #[getter] + #[pyo3(name = "trader_id")] + fn py_trader_id(&self) -> TraderId { + self.trader_id + } + + #[getter] + #[pyo3(name = "strategy_id")] + fn py_strategy_id(&self) -> StrategyId { + self.strategy_id + } + + #[getter] + #[pyo3(name = "instrument_id")] + fn py_instrument_id(&self) -> InstrumentId { + self.instrument_id + } + + #[getter] + #[pyo3(name = "client_order_id")] + fn py_client_order_id(&self) -> ClientOrderId { + self.client_order_id + } + + #[getter] + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { + self.order_type + } + + #[getter] + #[pyo3(name = "side")] + fn py_side(&self) -> OrderSide { + self.side + } + + #[getter] + #[pyo3(name = "quantity")] + fn py_quantity(&self) -> Quantity { + self.quantity + } + + #[getter] + #[pyo3(name = "price")] + fn py_price(&self) -> Price { + self.price + } + + #[getter] + #[pyo3(name = "expire_time")] + fn py_expire_time(&self) -> Option { + self.expire_time + } + + #[getter] + #[pyo3(name = "status")] + fn py_status(&self) -> OrderStatus { + self.status + } + + #[getter] + #[pyo3(name = "time_in_force")] + fn py_time_in_force(&self) -> TimeInForce { + self.time_in_force + } + + #[getter] + #[pyo3(name = "is_post_only")] + fn py_is_post_only(&self) -> bool { + self.is_post_only + } + + #[getter] + #[pyo3(name = "is_reduce_only")] + fn py_is_reduce_only(&self) -> bool { + self.is_reduce_only + } + + #[getter] + #[pyo3(name = "is_quote_quantity")] + fn py_is_quote_quantity(&self) -> bool { + self.is_quote_quantity + } + + #[getter] + #[pyo3(name = "has_price")] + fn py_has_price(&self) -> bool { + true + } + + #[getter] + #[pyo3(name = "has_trigger_price")] + fn py_trigger_price(&self) -> bool { + false + } + + #[getter] + #[pyo3(name = "is_passive")] + fn py_is_passive(&self) -> bool { + true + } + + #[getter] + #[pyo3(name = "is_open")] + fn py_is_open(&self) -> bool { + self.is_open() + } + + #[getter] + #[pyo3(name = "is_closed")] + fn py_is_closed(&self) -> bool { + self.is_closed() + } + + #[getter] + #[pyo3(name = "is_aggressive")] + fn py_is_aggressive(&self) -> bool { + self.is_aggressive() + } + + #[getter] + #[pyo3(name = "is_emulated")] + fn py_is_emulated(&self) -> bool { + self.is_emulated() + } + + #[getter] + #[pyo3(name = "is_active_local")] + fn py_is_active_local(&self) -> bool { + self.is_active_local() + } + + #[getter] + #[pyo3(name = "is_primary")] + fn py_is_primary(&self) -> bool { + self.is_primary() + } + + #[getter] + #[pyo3(name = "is_spawned")] + fn py_is_spawned(&self) -> bool { + self.is_spawned() + } + + #[getter] + #[pyo3(name = "liquidity_side")] + fn py_liquidity_side(&self) -> Option { + self.liquidity_side + } + + #[getter] + #[pyo3(name = "filled_qty")] + fn py_venue_order_id(&self) -> Quantity { + self.filled_qty + } + + #[getter] + #[pyo3(name = "trigger_instrument_id")] + fn py_trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + + #[getter] + #[pyo3(name = "contingency_type")] + fn py_contingency_type(&self) -> Option { + self.contingency_type + } + + #[getter] + #[pyo3(name = "order_list_id")] + fn py_order_list_id(&self) -> Option { + self.order_list_id + } + + #[getter] + #[pyo3(name = "linked_order_ids")] + fn py_linked_order_ids(&self) -> Option> { + self.linked_order_ids.clone() + } + + #[getter] + #[pyo3(name = "parent_order_id")] + fn py_parent_order_id(&self) -> Option { + self.parent_order_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_id")] + fn py_exec_algorithm_id(&self) -> Option { + self.exec_algorithm_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_params")] + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.clone().map(|x| { + x.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + } + + #[getter] + #[pyo3(name = "tags")] + fn py_tags(&self) -> Option { + self.tags.map(|x| x.to_string()) + } + + #[getter] + #[pyo3(name = "emulation_trigger")] + fn py_emulation_trigger(&self) -> Option { + self.emulation_trigger + } + + #[getter] + #[pyo3(name = "expire_time_ns")] + fn py_expire_time_ns(&self) -> Option { + self.expire_time + } + + #[getter] + #[pyo3(name = "exec_spawn_id")] + fn py_exec_spawn_id(&self) -> Option { + self.exec_spawn_id + } + + #[getter] + #[pyo3(name = "init_id")] + fn py_init_id(&self) -> UUID4 { + self.init_id + } + + #[getter] + #[pyo3(name = "display_qty")] + fn py_display_qty(&self) -> Option { + self.display_qty + } + + #[getter] + #[pyo3(name = "ts_init")] + fn py_ts_init(&self) -> UnixNanos { + self.ts_init + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + let dict = values.as_ref(py); + let trader_id = TraderId::from(dict.get_item("trader_id")?.unwrap().extract::<&str>()?); + let strategy_id = + StrategyId::from(dict.get_item("strategy_id")?.unwrap().extract::<&str>()?); + let instrument_id = + InstrumentId::from(dict.get_item("instrument_id")?.unwrap().extract::<&str>()?); + let client_order_id = ClientOrderId::from( + dict.get_item("client_order_id")? + .unwrap() + .extract::<&str>()?, + ); + let order_side = dict + .get_item("side")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let quantity = Quantity::from(dict.get_item("quantity")?.unwrap().extract::<&str>()?); + let price = Price::from(dict.get_item("price")?.unwrap().extract::<&str>()?); + let time_in_force = dict + .get_item("time_in_force")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let expire_time_ns = dict + .get_item("expire_time_ns") + .map(|x| x.and_then(|inner| inner.extract::().ok()))?; + let is_post_only = dict.get_item("is_post_only")?.unwrap().extract::()?; + let is_reduce_only = dict + .get_item("is_reduce_only")? + .unwrap() + .extract::()?; + let is_quote_quantity = dict + .get_item("is_quote_quantity")? + .unwrap() + .extract::()?; + let display_qty = dict + .get_item("display_qty")? + .unwrap() + .extract::>()?; + let emulation_trigger = dict.get_item("emulation_trigger").map(|x| { + x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()) + })?; + let trigger_instrument_id = dict.get_item("trigger_instrument_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let contingency_type = dict.get_item("contingency_type").map(|x| { + x.and_then(|inner| { + inner + .extract::<&str>() + .unwrap() + .parse::() + .ok() + }) + })?; + let order_list_id = dict.get_item("order_list_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let linked_order_ids = dict.get_item("linked_order_ids").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some( + item.iter() + .map(|x| x.parse::().unwrap()) + .collect(), + ), + Err(_) => None, + } + }) + })?; + let parent_order_id = dict.get_item("parent_order_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_id = dict.get_item("exec_algorithm_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_params = dict.get_item("exec_algorithm_params").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some(str_hashmap_to_ustr(item)), + Err(_) => None, + } + }) + })?; + let exec_spawn_id = dict.get_item("exec_spawn_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let tags = dict.get_item("tags").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => Some(Ustr::from(item)), + Err(_) => None, + } + }) + })?; + let init_id = dict + .get_item("init_id") + .map(|x| x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()))? + .unwrap(); + let ts_init = dict.get_item("ts_init")?.unwrap().extract::()?; + let limit_order = LimitOrder::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + price, + time_in_force, + expire_time_ns, + is_post_only, + is_reduce_only, + is_quote_quantity, + display_qty, + emulation_trigger, + trigger_instrument_id, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags, + init_id, + ts_init, + ) + .unwrap(); + Ok(limit_order) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("trader_id", self.trader_id.to_string())?; + dict.set_item("strategy_id", self.strategy_id.to_string())?; + dict.set_item("instrument_id", self.instrument_id.to_string())?; + dict.set_item("client_order_id", self.client_order_id.to_string())?; + dict.set_item("side", self.side.to_string())?; + dict.set_item("type", self.order_type.to_string())?; + dict.set_item("quantity", self.quantity.to_string())?; + dict.set_item("price", self.price.to_string())?; + dict.set_item("status", self.status.to_string())?; + dict.set_item("time_in_force", self.time_in_force.to_string())?; + dict.set_item("expire_time_ns", self.expire_time)?; + dict.set_item("is_post_only", self.is_post_only)?; + dict.set_item("is_reduce_only", self.is_reduce_only)?; + dict.set_item("is_quote_quantity", self.is_quote_quantity)?; + dict.set_item("filled_qty", self.filled_qty.to_string())?; + dict.set_item("init_id", self.init_id.to_string())?; + dict.set_item("ts_init", self.ts_init)?; + dict.set_item("ts_last", self.ts_last)?; + let commissions_dict = PyDict::new(py); + for (key, value) in &self.commissions { + commissions_dict.set_item(key.code.to_string(), value.to_string())?; + } + dict.set_item("commissions", commissions_dict)?; + self.venue_order_id.map_or_else( + || dict.set_item("venue_order_id", py.None()), + |x| dict.set_item("venue_order_id", x.to_string()), + )?; + self.display_qty.map_or_else( + || dict.set_item("display_qty", py.None()), + |x| dict.set_item("display_qty", x.to_string()), + )?; + self.emulation_trigger.map_or_else( + || dict.set_item("emulation_trigger", py.None()), + |x| dict.set_item("emulation_trigger", x.to_string()), + )?; + self.trigger_instrument_id.map_or_else( + || dict.set_item("trigger_instrument_id", py.None()), + |x| dict.set_item("trigger_instrument_id", x.to_string()), + )?; + self.contingency_type.map_or_else( + || dict.set_item("contingency_type", py.None()), + |x| dict.set_item("contingency_type", x.to_string()), + )?; + self.order_list_id.map_or_else( + || dict.set_item("order_list_id", py.None()), + |x| dict.set_item("order_list_id", x.to_string()), + )?; + self.linked_order_ids.clone().map_or_else( + || dict.set_item("linked_order_ids", py.None()), + |linked_order_ids| { + let lined_order_ids_list = + PyList::new(py, linked_order_ids.iter().map(|item| item.to_string())); + dict.set_item("linked_order_ids", lined_order_ids_list) + }, + )?; + self.parent_order_id.map_or_else( + || dict.set_item("parent_order_id", py.None()), + |x| dict.set_item("parent_order_id", x.to_string()), + )?; + self.exec_algorithm_id.map_or_else( + || dict.set_item("exec_algorithm_id", py.None()), + |x| dict.set_item("exec_algorithm_id", x.to_string()), + )?; + match &self.exec_algorithm_params { + Some(exec_algorithm_params) => { + let py_exec_algorithm_params = PyDict::new(py); + for (key, value) in exec_algorithm_params { + py_exec_algorithm_params.set_item(key.to_string(), value.to_string())?; + } + dict.set_item("exec_algorithm_params", py_exec_algorithm_params)?; + } + None => dict.set_item("exec_algorithm_params", py.None())?, + } + self.exec_spawn_id.map_or_else( + || dict.set_item("exec_spawn_id", py.None()), + |x| dict.set_item("exec_spawn_id", x.to_string()), + )?; + self.tags.map_or_else( + || dict.set_item("tags", py.None()), + |x| dict.set_item("tags", x.to_string()), + )?; + self.account_id.map_or_else( + || dict.set_item("account_id", py.None()), + |x| dict.set_item("account_id", x.to_string()), + )?; + self.slippage.map_or_else( + || dict.set_item("slippage", py.None()), + |x| dict.set_item("slippage", x.to_string()), + )?; + self.position_id.map_or_else( + || dict.set_item("position_id", py.None()), + |x| dict.set_item("position_id", x.to_string()), + )?; + self.liquidity_side.map_or_else( + || dict.set_item("liquidity_side", py.None()), + |x| dict.set_item("liquidity_side", x.to_string()), + )?; + self.last_trade_id.map_or_else( + || dict.set_item("last_trade_id", py.None()), + |x| dict.set_item("last_trade_id", x.to_string()), + )?; + self.avg_px.map_or_else( + || dict.set_item("avg_px", py.None()), + |x| dict.set_item("avg_px", x.to_string()), + )?; + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/orders/mod.rs b/nautilus_core/model/src/python/orders/mod.rs index 3ffe64791279..932a30dadb5e 100644 --- a/nautilus_core/model/src/python/orders/mod.rs +++ b/nautilus_core/model/src/python/orders/mod.rs @@ -13,4 +13,5 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +pub mod limit; pub mod market; diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index ce67b27fb0b0..2779045eb7a9 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -874,7 +874,87 @@ class VenueOrderId: ### Orders -class LimitOrder: ... +class LimitOrder: + def __init__( + self, + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + time_in_force: TimeInForce, + post_only: bool, + reduce_only: bool, + quote_quantity: bool, + init_id: UUID4, + ts_init: int, + expire_time: int | None = None, + display_qty: Quantity | None = None, + emulation_trigger: TriggerType | None = None, + trigger_instrument_id: InstrumentId | None = None, + contingency_type: ContingencyType | None = None, + order_list_id: OrderListId | None = None, + linked_order_ids: list[ClientOrderId] | None = None, + parent_order_id: ClientOrderId | None = None, + exec_algorithm_id: ExecAlgorithmId | None = None, + exec_algorithm_params: dict[str, str] | None = None, + exec_spawn_id: ClientOrderId | None = None, + tags: str | None = None, + ): ... + def to_dict(self) -> dict[str, str]: ... + @property + def trader_id(self) -> TraderId: ... + @property + def strategy_id(self) -> StrategyId: ... + @property + def instrument_id(self) -> InstrumentId: ... + @property + def client_order_id(self) -> ClientOrderId: ... + @property + def order_type(self) -> OrderType: ... + @property + def side(self) -> OrderSide: ... + @property + def quantity(self) -> Quantity: ... + @property + def price(self) -> Price: ... + @property + def expire_time(self) -> int | None: ... + @property + def status(self) -> OrderStatus: ... + @property + def time_in_force(self) -> TimeInForce: ... + @property + def is_post_only(self) -> bool: ... + @property + def is_reduce_only(self) -> bool: ... + @property + def is_quote_quantity(self) -> bool: ... + @property + def has_price(self) -> bool: ... + @property + def has_trigger_price(self) -> bool: ... + @property + def is_passive(self) -> bool: ... + @property + def is_aggressive(self) -> bool: ... + @property + def is_open(self) -> bool: ... + @property + def is_closed(self) -> bool: ... + @property + def is_emulated(self) -> bool: ... + @property + def is_active_local(self) -> bool: ... + @property + def is_primary(self) -> bool: ... + @property + def is_spawned(self) -> bool: ... + def from_dict(cls, values: dict[str, str]) -> LimitOrder: ... + + class LimitIfTouchedOrder: ... class MarketOrder: diff --git a/nautilus_trader/model/orders/limit.pxd b/nautilus_trader/model/orders/limit.pxd index cde5409a1134..7b4c03c620da 100644 --- a/nautilus_trader/model/orders/limit.pxd +++ b/nautilus_trader/model/orders/limit.pxd @@ -34,3 +34,6 @@ cdef class LimitOrder(Order): @staticmethod cdef LimitOrder transform(Order order, uint64_t ts_init, Price price=*) + + @staticmethod + cdef LimitOrder from_pyo3_c(pyo3_order) diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index f9d3fb170433..503e78fd97f2 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -26,11 +26,15 @@ from nautilus_trader.core.rust.model cimport TriggerType from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.events.order cimport OrderInitialized from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.functions cimport contingency_type_from_str from nautilus_trader.model.functions cimport contingency_type_to_str from nautilus_trader.model.functions cimport liquidity_side_to_str +from nautilus_trader.model.functions cimport order_side_from_str from nautilus_trader.model.functions cimport order_side_to_str from nautilus_trader.model.functions cimport order_type_to_str +from nautilus_trader.model.functions cimport time_in_force_from_str from nautilus_trader.model.functions cimport time_in_force_to_str +from nautilus_trader.model.functions cimport trigger_type_from_str from nautilus_trader.model.functions cimport trigger_type_to_str from nautilus_trader.model.identifiers cimport ClientOrderId from nautilus_trader.model.identifiers cimport ExecAlgorithmId @@ -257,6 +261,40 @@ cdef class LimitOrder(Order): f"{emulation_str}" ) + @staticmethod + cdef LimitOrder from_pyo3_c(pyo3_order): + return LimitOrder( + trader_id=TraderId(str(pyo3_order.trader_id)), + strategy_id=StrategyId(str(pyo3_order.strategy_id)), + instrument_id=InstrumentId.from_str_c(str(pyo3_order.instrument_id)), + client_order_id=ClientOrderId(str(pyo3_order.client_order_id)), + order_side=order_side_from_str(str(pyo3_order.side)), + quantity=Quantity.from_raw_c(pyo3_order.quantity.raw, pyo3_order.quantity.precision), + price=Price.from_raw_c(pyo3_order.price.raw, pyo3_order.price.precision), + init_id=UUID4(str(pyo3_order.init_id)), + ts_init=pyo3_order.ts_init, + time_in_force=time_in_force_from_str(str(pyo3_order.time_in_force)), + expire_time_ns=int(pyo3_order.expire_time_ns) if pyo3_order.expire_time_ns is not None else 0, + post_only=pyo3_order.is_post_only, + reduce_only=pyo3_order.is_reduce_only, + quote_quantity=pyo3_order.is_quote_quantity, + display_qty=Quantity.from_str_c(pyo3_order.display_qty) if pyo3_order.display_qty is not None else None, + emulation_trigger=trigger_type_from_str(str(pyo3_order.emulation_trigger)) if pyo3_order.emulation_trigger is not None else TriggerType.NO_TRIGGER, + trigger_instrument_id=InstrumentId.from_str_c(str(pyo3_order.trigger_instrument_id)) if pyo3_order.trigger_instrument_id is not None else None, + contingency_type=contingency_type_from_str(str(pyo3_order.contingency_type)) if pyo3_order.contingency_type is not None else ContingencyType.NO_CONTINGENCY, + order_list_id=OrderListId(pyo3_order.order_list_id) if pyo3_order.order_list_id is not None else None, + linked_order_ids=[ClientOrderId(str(o)) for o in pyo3_order.linked_order_ids] if pyo3_order.linked_order_ids is not None else None, + parent_order_id=ClientOrderId(str(pyo3_order.parent_order_id)) if pyo3_order.parent_order_id is not None else None, + exec_algorithm_id=ExecAlgorithmId(str(pyo3_order.exec_algorithm_id)) if pyo3_order.exec_algorithm_id is not None else None, + exec_algorithm_params=pyo3_order.exec_algorithm_params, + exec_spawn_id=ClientOrderId(str(pyo3_order.exec_spawn_id)) if pyo3_order.exec_spawn_id is not None else None, + tags=pyo3_order.tags if pyo3_order.tags is not None else None, + ) + + @staticmethod + def from_pyo3(pyo3_order): + return LimitOrder.from_pyo3_c(pyo3_order) + cpdef dict to_dict(self): """ Return a dictionary representation of this object. @@ -286,7 +324,7 @@ cdef class LimitOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, "status": self._fsm.state_string_c(), "is_post_only": self.is_post_only, "is_reduce_only": self.is_reduce_only, @@ -302,6 +340,7 @@ cdef class LimitOrder(Order): "exec_algorithm_params": self.exec_algorithm_params, "exec_spawn_id": self.exec_spawn_id.to_str() if self.exec_spawn_id is not None else None, "tags": self.tags, + "init_id": str(self.init_id), "ts_init": self.ts_init, "ts_last": self.ts_last, } diff --git a/nautilus_trader/test_kit/rust/orders_pyo3.py b/nautilus_trader/test_kit/rust/orders_pyo3.py index 67021cb99edf..1919de81fd62 100644 --- a/nautilus_trader/test_kit/rust/orders_pyo3.py +++ b/nautilus_trader/test_kit/rust/orders_pyo3.py @@ -14,24 +14,29 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.core.nautilus_pyo3 import ClientOrderId +from nautilus_trader.core.nautilus_pyo3 import ExecAlgorithmId +from nautilus_trader.core.nautilus_pyo3 import InstrumentId +from nautilus_trader.core.nautilus_pyo3 import LimitOrder from nautilus_trader.core.nautilus_pyo3 import MarketOrder from nautilus_trader.core.nautilus_pyo3 import OrderSide +from nautilus_trader.core.nautilus_pyo3 import Price from nautilus_trader.core.nautilus_pyo3 import Quantity from nautilus_trader.core.nautilus_pyo3 import StrategyId from nautilus_trader.core.nautilus_pyo3 import TimeInForce +from nautilus_trader.core.nautilus_pyo3 import TraderId from nautilus_trader.test_kit.rust.identifiers_pyo3 import TestIdProviderPyo3 class TestOrderProviderPyo3: @staticmethod def market_order( - instrument_id=None, - order_side=None, - quantity=None, - trader_id=None, + instrument_id: InstrumentId | None = None, + order_side: OrderSide | None = None, + quantity: Quantity | None = None, + trader_id: TraderId | None = None, strategy_id: StrategyId | None = None, client_order_id: ClientOrderId | None = None, - time_in_force=None, + time_in_force: TimeInForce | None = None, ) -> MarketOrder: return MarketOrder( trader_id=trader_id or TestIdProviderPyo3.trader_id(), @@ -50,3 +55,33 @@ def market_order( parent_order_id=None, tags=None, ) + + @staticmethod + def limit_order( + instrument_id: InstrumentId, + order_side: OrderSide, + quantity: Quantity, + price: Price, + trader_id: TraderId | None = None, + strategy_id: StrategyId | None = None, + client_order_id: ClientOrderId | None = None, + time_in_force: TimeInForce | None = None, + exec_algorithm_id: ExecAlgorithmId | None = None, + ) -> LimitOrder: + return LimitOrder( + trader_id=trader_id or TestIdProviderPyo3.trader_id(), + strategy_id=strategy_id or TestIdProviderPyo3.strategy_id(), + instrument_id=instrument_id or TestIdProviderPyo3.audusd_id(), + client_order_id=client_order_id or TestIdProviderPyo3.client_order_id(1), + order_side=order_side or OrderSide.BUY, + quantity=quantity or Quantity.from_str("100"), + time_in_force=time_in_force or TimeInForce.GTC, + price=price, + post_only=False, + reduce_only=False, + quote_quantity=False, + init_id=TestIdProviderPyo3.uuid(), + ts_init=0, + exec_algorithm_id=exec_algorithm_id, + exec_spawn_id=TestIdProviderPyo3.client_order_id(1), + ) diff --git a/tests/unit_tests/model/instruments/__init__.py b/tests/unit_tests/model/instruments/__init__.py index e69de29bb2d1..3d34cab4588e 100644 --- a/tests/unit_tests/model/instruments/__init__.py +++ b/tests/unit_tests/model/instruments/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/objects/__init__.py b/tests/unit_tests/model/objects/__init__.py index e69de29bb2d1..3d34cab4588e 100644 --- a/tests/unit_tests/model/objects/__init__.py +++ b/tests/unit_tests/model/objects/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/orders/__init__.py b/tests/unit_tests/model/orders/__init__.py new file mode 100644 index 000000000000..3d34cab4588e --- /dev/null +++ b/tests/unit_tests/model/orders/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/model/orders/test_limit_order_pyo3.py b/tests/unit_tests/model/orders/test_limit_order_pyo3.py new file mode 100644 index 000000000000..d1d79bb19d0a --- /dev/null +++ b/tests/unit_tests/model/orders/test_limit_order_pyo3.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.core import nautilus_pyo3 +from nautilus_trader.core.nautilus_pyo3 import ExecAlgorithmId +from nautilus_trader.core.nautilus_pyo3 import InstrumentId +from nautilus_trader.core.nautilus_pyo3 import OrderSide +from nautilus_trader.core.nautilus_pyo3 import OrderStatus +from nautilus_trader.core.nautilus_pyo3 import OrderType +from nautilus_trader.core.nautilus_pyo3 import Price +from nautilus_trader.core.nautilus_pyo3 import Quantity +from nautilus_trader.core.nautilus_pyo3 import TimeInForce +from nautilus_trader.model.orders import LimitOrder +from nautilus_trader.test_kit.rust.orders_pyo3 import TestOrderProviderPyo3 + + +AUDUSD_SIM = InstrumentId.from_str("AUD/USD.SIM") + + +def test_initialize_limit_order(): + order = TestOrderProviderPyo3.limit_order( + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(100_000), + price=Price.from_str("1.00000"), + exec_algorithm_id=ExecAlgorithmId("TWAP"), + ) + + # Assert + assert order.order_type == OrderType.LIMIT + assert order.expire_time == 0 + assert order.status == OrderStatus.INITIALIZED + assert order.time_in_force == TimeInForce.GTC + assert order.has_price + assert not order.has_trigger_price + assert order.is_passive + assert not order.is_open + assert not order.is_aggressive + assert not order.is_closed + assert not order.is_emulated + assert order.is_active_local + assert order.is_primary + assert not order.is_spawned + assert ( + str(order) + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, " + + "client_order_id=O-20210410-022422-001-001-1, venue_order_id=None, position_id=None, " + + "exec_algorithm_id=TWAP, exec_spawn_id=O-20210410-022422-001-001-1, tags=None)" + ) + assert ( + repr(order) + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, " + + "client_order_id=O-20210410-022422-001-001-1, venue_order_id=None, position_id=None, " + + "exec_algorithm_id=TWAP, exec_spawn_id=O-20210410-022422-001-001-1, tags=None)" + ) + + +def test_pyo3_cython_conversion(): + limit_order_pyo3 = TestOrderProviderPyo3.limit_order( + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(100_000), + price=Price.from_str("1.00000"), + ) + limit_order_pyo3_dict = limit_order_pyo3.to_dict() + limit_order_cython = LimitOrder.from_pyo3(limit_order_pyo3) + limit_order_cython_dict = LimitOrder.to_dict(limit_order_cython) + limit_order_pyo3_back = nautilus_pyo3.LimitOrder.from_dict(limit_order_cython_dict) + assert limit_order_pyo3_dict == limit_order_cython_dict + assert limit_order_pyo3 == limit_order_pyo3_back diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 0a5b8fcf141a..6e205f73f8dc 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -468,6 +468,8 @@ def test_limit_order_to_dict(self): # Act result = order.to_dict() + # remove init_id as it non-deterministic with order-factory + del result["init_id"] # Assert assert result == { @@ -489,7 +491,7 @@ def test_limit_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, "status": "INITIALIZED", "is_post_only": False, "is_reduce_only": False, From 1029ba2c2a208d4ef39186d02053e9b5b247c111 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Mar 2024 20:33:56 +1100 Subject: [PATCH 56/71] Fix clippy lints --- nautilus_core/model/src/orders/base.rs | 2 ++ nautilus_core/model/src/orders/limit.rs | 6 +++--- nautilus_core/model/src/python/orders/limit.rs | 12 ++++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 1b3a328418a8..9bbd052f1577 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -500,6 +500,7 @@ pub trait Order { fn is_pending_cancel(&self) -> bool { self.status() == OrderStatus::PendingCancel } + fn is_spawned(&self) -> bool { self.exec_algorithm_id().is_some() && self.exec_spawn_id().unwrap() != self.client_order_id() @@ -843,6 +844,7 @@ impl OrderCore { self.commissions.clone() } + #[must_use] pub fn init_event(&self) -> Option<&OrderEvent> { self.events.first() } diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 8eebacdfc8aa..0710b20556f3 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -19,7 +19,6 @@ use std::{ ops::{Deref, DerefMut}, }; -use anyhow::bail; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; use ustr::Ustr; @@ -90,14 +89,15 @@ impl LimitOrder { check_quantity_positive(quantity)?; if time_in_force == TimeInForce::Gtd { if expire_time.is_none() { - bail!("Condition failed: `expire_time` is required for `GTD` order") + anyhow::bail!("Condition failed: `expire_time` is required for `GTD` order") } if let Some(time) = expire_time { if time == 0 { - bail!("`expire_time` for `GTD` Limit order should be higher then 0") + anyhow::bail!("`expire_time` for `GTD` Limit order should be higher then 0") } } } + Ok(Self { core: OrderCore::new( trader_id, diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 00a4f61b4b2e..3e375e76d43d 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -497,7 +497,7 @@ impl LimitOrder { .map(|x| x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()))? .unwrap(); let ts_init = dict.get_item("ts_init")?.unwrap().extract::()?; - let limit_order = LimitOrder::new( + let limit_order = Self::new( trader_id, strategy_id, instrument_id, @@ -581,9 +581,13 @@ impl LimitOrder { self.linked_order_ids.clone().map_or_else( || dict.set_item("linked_order_ids", py.None()), |linked_order_ids| { - let lined_order_ids_list = - PyList::new(py, linked_order_ids.iter().map(|item| item.to_string())); - dict.set_item("linked_order_ids", lined_order_ids_list) + let linked_order_ids_list = PyList::new( + py, + linked_order_ids + .iter() + .map(std::string::ToString::to_string), + ); + dict.set_item("linked_order_ids", linked_order_ids_list) }, )?; self.parent_order_id.map_or_else( From a238f6cb0c0d9898dc6acbdf9a314919006455da Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Tue, 19 Mar 2024 21:36:29 +0100 Subject: [PATCH 57/71] MarketOrder pyo3 Rust Cython conversion (#1553) --- nautilus_core/model/src/orders/market.rs | 6 + .../model/src/python/orders/market.rs | 401 ++++++++++++++++-- nautilus_trader/core/nautilus_pyo3.pyi | 1 + nautilus_trader/model/orders/limit.pyx | 2 +- nautilus_trader/model/orders/market.pxd | 3 + nautilus_trader/model/orders/market.pyx | 36 +- nautilus_trader/test_kit/rust/orders_pyo3.py | 8 +- .../test_market_order_pyo3.py} | 57 +-- tests/unit_tests/model/test_orders.py | 5 +- 9 files changed, 455 insertions(+), 64 deletions(-) rename tests/unit_tests/model/{test_orders_pyo3.py => orders/test_market_order_pyo3.py} (73%) diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 1f382302b751..99a374ac23b3 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -120,6 +120,12 @@ impl DerefMut for MarketOrder { } } +impl PartialEq for MarketOrder { + fn eq(&self, other: &Self) -> bool { + self.client_order_id == other.client_order_id + } +} + impl Order for MarketOrder { fn status(&self) -> OrderStatus { self.status diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index 502b621932c7..a9125561256a 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -16,7 +16,12 @@ use std::collections::HashMap; use nautilus_core::{python::to_pyvalue_err, time::UnixNanos, uuid::UUID4}; -use pyo3::{pymethods, PyResult}; +use pyo3::{ + basic::CompareOp, + pymethods, + types::{PyDict, PyList}, + IntoPy, Py, PyAny, PyObject, PyResult, Python, +}; use rust_decimal::Decimal; use ustr::Ustr; @@ -37,27 +42,6 @@ use crate::{ #[pymethods] impl MarketOrder { #[new] - #[pyo3(signature = ( - trader_id, - strategy_id, - instrument_id, - client_order_id, - order_side, - quantity, - init_id, - ts_init, - time_in_force=TimeInForce::Gtd, - reduce_only=false, - quote_quantity=false, - contingency_type=None, - order_list_id=None, - linked_order_ids=None, - parent_order_id=None, - exec_algorithm_id=None, - exec_algorithm_params=None, - exec_spawn_id=None, - tags=None, - ))] #[allow(clippy::too_many_arguments)] fn py_new( trader_id: TraderId, @@ -104,6 +88,13 @@ impl MarketOrder { .map_err(to_pyvalue_err) } + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + #[staticmethod] #[pyo3(name = "opposite_side")] fn py_opposite_side(side: OrderSide) -> OrderSide { @@ -135,37 +126,385 @@ impl MarketOrder { fn py_commissions(&self) -> HashMap { self.commissions() } + #[getter] - fn account_id(&self) -> Option { + #[pyo3(name = "account_id")] + fn py_account_id(&self) -> Option { self.account_id } + #[getter] - fn instrument_id(&self) -> InstrumentId { + #[pyo3(name = "instrument_id")] + fn py_instrument_id(&self) -> InstrumentId { self.instrument_id } + #[getter] - fn trader_id(&self) -> TraderId { + #[pyo3(name = "trader_id")] + fn py_trader_id(&self) -> TraderId { self.trader_id } #[getter] - fn client_order_id(&self) -> ClientOrderId { + #[pyo3(name = "strategy_id")] + fn py_strategy_id(&self) -> StrategyId { + self.strategy_id + } + + #[getter] + #[pyo3(name = "init_id")] + fn py_init_id(&self) -> UUID4 { + self.init_id + } + + #[getter] + #[pyo3(name = "ts_init")] + fn py_ts_init(&self) -> UnixNanos { + self.ts_init + } + + #[getter] + #[pyo3(name = "client_order_id")] + fn py_client_order_id(&self) -> ClientOrderId { self.client_order_id } + + #[getter] + #[pyo3(name = "order_list_id")] + fn py_order_list_id(&self) -> Option { + self.order_list_id + } + + #[getter] + #[pyo3(name = "linked_order_ids")] + fn py_linked_order_ids(&self) -> Option> { + self.linked_order_ids.clone() + } + + #[getter] + #[pyo3(name = "parent_order_id")] + fn py_parent_order_id(&self) -> Option { + self.parent_order_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_id")] + fn py_exec_algorithm_id(&self) -> Option { + self.exec_algorithm_id + } + + #[getter] + #[pyo3(name = "exec_algorithm_params")] + fn py_exec_algorithm_params(&self) -> Option> { + self.exec_algorithm_params.clone().map(|x| { + x.into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() + }) + } + + #[getter] + #[pyo3(name = "exec_spawn_id")] + fn py_exec_spawn_id(&self) -> Option { + self.exec_spawn_id + } + + #[getter] + #[pyo3(name = "is_reduce_only")] + fn py_is_reduce_only(&self) -> bool { + self.is_reduce_only + } + #[getter] - fn quantity(&self) -> Quantity { + #[pyo3(name = "is_quote_quantity")] + fn py_is_quote_quantity(&self) -> bool { + self.is_quote_quantity + } + + #[getter] + #[pyo3(name = "contingency_type")] + fn py_contingency_type(&self) -> Option { + self.contingency_type + } + + #[getter] + #[pyo3(name = "quantity")] + fn py_quantity(&self) -> Quantity { self.quantity } + #[getter] - fn side(&self) -> OrderSide { + #[pyo3(name = "side")] + fn py_side(&self) -> OrderSide { self.side } + #[getter] - fn order_type(&self) -> OrderType { + #[pyo3(name = "order_type")] + fn py_order_type(&self) -> OrderType { self.order_type } + #[getter] - fn strategy_id(&self) -> StrategyId { - self.strategy_id + #[pyo3(name = "emulation_trigger")] + fn py_emulation_trigger(&self) -> Option { + self.emulation_trigger.map(|x| x.to_string()) + } + + #[getter] + #[pyo3(name = "time_in_force")] + fn py_time_in_force(&self) -> TimeInForce { + self.time_in_force + } + + #[getter] + #[pyo3(name = "tags")] + fn py_tags(&self) -> Option { + self.tags.map(|x| x.to_string()) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("trader_id", self.trader_id.to_string())?; + dict.set_item("strategy_id", self.strategy_id.to_string())?; + dict.set_item("instrument_id", self.instrument_id.to_string())?; + dict.set_item("client_order_id", self.client_order_id.to_string())?; + dict.set_item("side", self.side.to_string())?; + dict.set_item("type", self.order_type.to_string())?; + dict.set_item("quantity", self.quantity.to_string())?; + dict.set_item("status", self.status.to_string())?; + dict.set_item("time_in_force", self.time_in_force.to_string())?; + dict.set_item("is_reduce_only", self.is_reduce_only)?; + dict.set_item("is_quote_quantity", self.is_quote_quantity)?; + dict.set_item("filled_qty", self.filled_qty.to_string())?; + dict.set_item("init_id", self.init_id.to_string())?; + dict.set_item("ts_init", self.ts_init)?; + dict.set_item("ts_last", self.ts_last)?; + let commissions_dict = PyDict::new(py); + for (key, value) in &self.commissions { + commissions_dict.set_item(key.code.to_string(), value.to_string())?; + } + dict.set_item("commissions", commissions_dict)?; + self.venue_order_id.map_or_else( + || dict.set_item("venue_order_id", py.None()), + |x| dict.set_item("venue_order_id", x.to_string()), + )?; + self.emulation_trigger.map_or_else( + || dict.set_item("emulation_trigger", py.None()), + |x| dict.set_item("emulation_trigger", x.to_string()), + )?; + self.contingency_type.map_or_else( + || dict.set_item("contingency_type", py.None()), + |x| dict.set_item("contingency_type", x.to_string()), + )?; + self.order_list_id.map_or_else( + || dict.set_item("order_list_id", py.None()), + |x| dict.set_item("order_list_id", x.to_string()), + )?; + self.linked_order_ids.clone().map_or_else( + || dict.set_item("linked_order_ids", py.None()), + |linked_order_ids| { + let linked_order_ids_list = PyList::new( + py, + linked_order_ids + .iter() + .map(std::string::ToString::to_string), + ); + dict.set_item("linked_order_ids", linked_order_ids_list) + }, + )?; + self.parent_order_id.map_or_else( + || dict.set_item("parent_order_id", py.None()), + |x| dict.set_item("parent_order_id", x.to_string()), + )?; + self.exec_algorithm_id.map_or_else( + || dict.set_item("exec_algorithm_id", py.None()), + |x| dict.set_item("exec_algorithm_id", x.to_string()), + )?; + match &self.exec_algorithm_params { + Some(exec_algorithm_params) => { + let py_exec_algorithm_params = PyDict::new(py); + for (key, value) in exec_algorithm_params { + py_exec_algorithm_params.set_item(key.to_string(), value.to_string())?; + } + dict.set_item("exec_algorithm_params", py_exec_algorithm_params)?; + } + None => dict.set_item("exec_algorithm_params", py.None())?, + } + self.exec_spawn_id.map_or_else( + || dict.set_item("exec_spawn_id", py.None()), + |x| dict.set_item("exec_spawn_id", x.to_string()), + )?; + self.tags.map_or_else( + || dict.set_item("tags", py.None()), + |x| dict.set_item("tags", x.to_string()), + )?; + self.account_id.map_or_else( + || dict.set_item("account_id", py.None()), + |x| dict.set_item("account_id", x.to_string()), + )?; + self.slippage.map_or_else( + || dict.set_item("slippage", py.None()), + |x| dict.set_item("slippage", x.to_string()), + )?; + self.position_id.map_or_else( + || dict.set_item("position_id", py.None()), + |x| dict.set_item("position_id", x.to_string()), + )?; + self.liquidity_side.map_or_else( + || dict.set_item("liquidity_side", py.None()), + |x| dict.set_item("liquidity_side", x.to_string()), + )?; + self.last_trade_id.map_or_else( + || dict.set_item("last_trade_id", py.None()), + |x| dict.set_item("last_trade_id", x.to_string()), + )?; + self.avg_px.map_or_else( + || dict.set_item("avg_px", py.None()), + |x| dict.set_item("avg_px", x.to_string()), + )?; + Ok(dict.into()) + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + let dict = values.as_ref(py); + let trader_id = TraderId::from(dict.get_item("trader_id")?.unwrap().extract::<&str>()?); + let strategy_id = + StrategyId::from(dict.get_item("strategy_id")?.unwrap().extract::<&str>()?); + let instrument_id = + InstrumentId::from(dict.get_item("instrument_id")?.unwrap().extract::<&str>()?); + let client_order_id = ClientOrderId::from( + dict.get_item("client_order_id")? + .unwrap() + .extract::<&str>()?, + ); + let order_side = dict + .get_item("side")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let quantity = Quantity::from(dict.get_item("quantity")?.unwrap().extract::<&str>()?); + let time_in_force = dict + .get_item("time_in_force")? + .unwrap() + .extract::<&str>()? + .parse::() + .unwrap(); + let init_id = dict + .get_item("init_id") + .map(|x| x.and_then(|inner| inner.extract::<&str>().unwrap().parse::().ok()))? + .unwrap(); + let ts_init = dict.get_item("ts_init")?.unwrap().extract::()?; + let is_reduce_only = dict + .get_item("is_reduce_only")? + .unwrap() + .extract::()?; + let is_quote_quantity = dict + .get_item("is_quote_quantity")? + .unwrap() + .extract::()?; + let contingency_type = dict.get_item("contingency_type").map(|x| { + x.and_then(|inner| { + inner + .extract::<&str>() + .unwrap() + .parse::() + .ok() + }) + })?; + let order_list_id = dict.get_item("order_list_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let linked_order_ids = dict.get_item("linked_order_ids").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some( + item.iter() + .map(|x| x.parse::().unwrap()) + .collect(), + ), + Err(_) => None, + } + }) + })?; + let parent_order_id = dict.get_item("parent_order_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_id = dict.get_item("exec_algorithm_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let exec_algorithm_params = dict.get_item("exec_algorithm_params").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::>(); + match extracted_str { + Ok(item) => Some(str_hashmap_to_ustr(item)), + Err(_) => None, + } + }) + })?; + let exec_spawn_id = dict.get_item("exec_spawn_id").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => item.parse::().ok(), + Err(_) => None, + } + }) + })?; + let tags = dict.get_item("tags").map(|x| { + x.and_then(|inner| { + let extracted_str = inner.extract::<&str>(); + match extracted_str { + Ok(item) => Some(Ustr::from(item)), + Err(_) => None, + } + }) + })?; + let market_order = Self::new( + trader_id, + strategy_id, + instrument_id, + client_order_id, + order_side, + quantity, + time_in_force, + init_id, + ts_init, + is_reduce_only, + is_quote_quantity, + contingency_type, + order_list_id, + linked_order_ids, + parent_order_id, + exec_algorithm_id, + exec_algorithm_params, + exec_spawn_id, + tags, + ) + .unwrap(); + Ok(market_order) } } diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 2779045eb7a9..7ab9b8b908b0 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -980,6 +980,7 @@ class MarketOrder: exec_spawn_id: ClientOrderId | None = None, tags: str | None = None, ) -> None: ... + def to_dict(self) -> dict[str, str]: ... @staticmethod def opposite_side(side: OrderSide) -> OrderSide: ... @staticmethod diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 503e78fd97f2..86ba7aa4b189 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -282,7 +282,7 @@ cdef class LimitOrder(Order): emulation_trigger=trigger_type_from_str(str(pyo3_order.emulation_trigger)) if pyo3_order.emulation_trigger is not None else TriggerType.NO_TRIGGER, trigger_instrument_id=InstrumentId.from_str_c(str(pyo3_order.trigger_instrument_id)) if pyo3_order.trigger_instrument_id is not None else None, contingency_type=contingency_type_from_str(str(pyo3_order.contingency_type)) if pyo3_order.contingency_type is not None else ContingencyType.NO_CONTINGENCY, - order_list_id=OrderListId(pyo3_order.order_list_id) if pyo3_order.order_list_id is not None else None, + order_list_id=OrderListId(str(pyo3_order.order_list_id)) if pyo3_order.order_list_id is not None else None, linked_order_ids=[ClientOrderId(str(o)) for o in pyo3_order.linked_order_ids] if pyo3_order.linked_order_ids is not None else None, parent_order_id=ClientOrderId(str(pyo3_order.parent_order_id)) if pyo3_order.parent_order_id is not None else None, exec_algorithm_id=ExecAlgorithmId(str(pyo3_order.exec_algorithm_id)) if pyo3_order.exec_algorithm_id is not None else None, diff --git a/nautilus_trader/model/orders/market.pxd b/nautilus_trader/model/orders/market.pxd index 186e0ca38b9d..96e89d71d120 100644 --- a/nautilus_trader/model/orders/market.pxd +++ b/nautilus_trader/model/orders/market.pxd @@ -25,3 +25,6 @@ cdef class MarketOrder(Order): @staticmethod cdef MarketOrder transform(Order order, uint64_t ts_init) + + @staticmethod + cdef MarketOrder from_pyo3_c(pyo3_order) diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 615d9d6b3d5c..fc5532193064 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -24,11 +24,15 @@ from nautilus_trader.core.rust.model cimport TriggerType from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.events.order cimport OrderInitialized from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.functions cimport contingency_type_from_str from nautilus_trader.model.functions cimport contingency_type_to_str from nautilus_trader.model.functions cimport liquidity_side_to_str +from nautilus_trader.model.functions cimport order_side_from_str from nautilus_trader.model.functions cimport order_side_to_str from nautilus_trader.model.functions cimport order_type_to_str +from nautilus_trader.model.functions cimport time_in_force_from_str from nautilus_trader.model.functions cimport time_in_force_to_str +from nautilus_trader.model.functions cimport trigger_type_to_str from nautilus_trader.model.identifiers cimport ClientOrderId from nautilus_trader.model.identifiers cimport ExecAlgorithmId from nautilus_trader.model.identifiers cimport InstrumentId @@ -188,6 +192,34 @@ cdef class MarketOrder(Order): f"{time_in_force_to_str(self.time_in_force)}" ) + @staticmethod + cdef MarketOrder from_pyo3_c(pyo3_order): + return MarketOrder( + trader_id=TraderId(str(pyo3_order.trader_id)), + strategy_id=StrategyId(str(pyo3_order.strategy_id)), + instrument_id=InstrumentId.from_str_c(str(pyo3_order.instrument_id)), + client_order_id=ClientOrderId(str(pyo3_order.client_order_id)), + order_side=order_side_from_str(str(pyo3_order.side)), + quantity=Quantity.from_raw_c(pyo3_order.quantity.raw, pyo3_order.quantity.precision), + init_id=UUID4(str(pyo3_order.init_id)), + ts_init=pyo3_order.ts_init, + time_in_force=time_in_force_from_str(str(pyo3_order.time_in_force)), + reduce_only=pyo3_order.is_reduce_only, + quote_quantity=pyo3_order.is_quote_quantity, + contingency_type=contingency_type_from_str(str(pyo3_order.contingency_type)) if pyo3_order.contingency_type is not None else ContingencyType.NO_CONTINGENCY, + order_list_id=OrderListId(str(pyo3_order.order_list_id)) if pyo3_order.order_list_id is not None else None, + linked_order_ids=[ClientOrderId(str(o)) for o in pyo3_order.linked_order_ids] if pyo3_order.linked_order_ids is not None else None, + parent_order_id=ClientOrderId(str(pyo3_order.parent_order_id)) if pyo3_order.parent_order_id is not None else None, + exec_algorithm_id=ExecAlgorithmId(str(pyo3_order.exec_algorithm_id)) if pyo3_order.exec_algorithm_id is not None else None, + exec_algorithm_params=pyo3_order.exec_algorithm_params, + exec_spawn_id=ClientOrderId(str(pyo3_order.exec_spawn_id)) if pyo3_order.exec_spawn_id is not None else None, + tags=pyo3_order.tags if pyo3_order.tags is not None else None, + ) + + @staticmethod + def from_pyo3(pyo3_order): + return MarketOrder.from_pyo3_c(pyo3_order) + cpdef dict to_dict(self): """ Return a dictionary representation of this object. @@ -217,7 +249,8 @@ cdef class MarketOrder(Order): "liquidity_side": liquidity_side_to_str(self.liquidity_side), "avg_px": str(self.avg_px) if self.filled_qty.as_f64_c() > 0.0 else None, "slippage": str(self.slippage) if self.filled_qty.as_f64_c() > 0.0 else None, - "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else None, + "commissions": str([c.to_str() for c in self.commissions()]) if self._commissions else {}, + "emulation_trigger": trigger_type_to_str(self.emulation_trigger), "status": self._fsm.state_string_c(), "contingency_type": contingency_type_to_str(self.contingency_type), "order_list_id": self.order_list_id.to_str() if self.order_list_id is not None else None, @@ -227,6 +260,7 @@ cdef class MarketOrder(Order): "exec_algorithm_params": self.exec_algorithm_params, "exec_spawn_id": self.exec_spawn_id.to_str() if self.exec_spawn_id is not None else None, "tags": self.tags, + "init_id": str(self.init_id), "ts_init": self.ts_init, "ts_last": self.ts_last, } diff --git a/nautilus_trader/test_kit/rust/orders_pyo3.py b/nautilus_trader/test_kit/rust/orders_pyo3.py index 1919de81fd62..86a68693795e 100644 --- a/nautilus_trader/test_kit/rust/orders_pyo3.py +++ b/nautilus_trader/test_kit/rust/orders_pyo3.py @@ -46,14 +46,10 @@ def market_order( order_side=order_side or OrderSide.BUY, quantity=quantity or Quantity.from_str("100"), time_in_force=time_in_force or TimeInForce.GTC, + reduce_only=False, + quote_quantity=False, init_id=TestIdProviderPyo3.uuid(), ts_init=0, - reduce_only=False, - contingency_type=None, - order_list_id=None, - linked_order_ids=None, - parent_order_id=None, - tags=None, ) @staticmethod diff --git a/tests/unit_tests/model/test_orders_pyo3.py b/tests/unit_tests/model/orders/test_market_order_pyo3.py similarity index 73% rename from tests/unit_tests/model/test_orders_pyo3.py rename to tests/unit_tests/model/orders/test_market_order_pyo3.py index cd80a9b0e338..a76f88beac29 100644 --- a/tests/unit_tests/model/test_orders_pyo3.py +++ b/tests/unit_tests/model/orders/test_market_order_pyo3.py @@ -15,17 +15,16 @@ import pytest -from nautilus_trader.core.nautilus_pyo3 import UUID4 +from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.nautilus_pyo3 import AccountId -from nautilus_trader.core.nautilus_pyo3 import ClientOrderId from nautilus_trader.core.nautilus_pyo3 import InstrumentId -from nautilus_trader.core.nautilus_pyo3 import MarketOrder from nautilus_trader.core.nautilus_pyo3 import OrderSide from nautilus_trader.core.nautilus_pyo3 import PositionSide from nautilus_trader.core.nautilus_pyo3 import Quantity from nautilus_trader.core.nautilus_pyo3 import StrategyId from nautilus_trader.core.nautilus_pyo3 import TimeInForce from nautilus_trader.core.nautilus_pyo3 import TraderId +from nautilus_trader.model.orders import MarketOrder from nautilus_trader.test_kit.rust.orders_pyo3 import TestOrderProviderPyo3 @@ -49,7 +48,7 @@ ) def test_opposite_side_returns_expected_sides(side, expected): # Arrange, Act - result = MarketOrder.opposite_side(side) + result = nautilus_pyo3.MarketOrder.opposite_side(side) # Assert assert result == expected @@ -67,7 +66,7 @@ def test_closing_side_returns_expected_sides( expected: OrderSide, ) -> None: # Arrange, Act - result = MarketOrder.closing_side(side) + result = nautilus_pyo3.MarketOrder.closing_side(side) # Assert assert result == expected @@ -110,29 +109,39 @@ def test_would_reduce_only_with_various_values_returns_expected( def test_market_order_with_quantity_zero_raises_value_error(): # Arrange, Act, Assert with pytest.raises(ValueError): - MarketOrder( - trader_id, - strategy_id, - AUDUSD_SIM, - ClientOrderId("O-123456"), - OrderSide.BUY, - Quantity.zero(), # <- invalid - UUID4(), - 0, + TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(0), ) def test_market_order_with_invalid_tif_raises_value_error(): # Arrange, Act, Assert with pytest.raises(ValueError): - MarketOrder( - trader_id, - strategy_id, - AUDUSD_SIM, - ClientOrderId("O-123456"), - OrderSide.BUY, - Quantity.from_int(100_000), - UUID4(), - 0, - TimeInForce.GTD, # <-- invalid + TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(0), + time_in_force=TimeInForce.GTD, ) + + +def test_pyo3_cython_conversion(): + market_order_pyo3 = TestOrderProviderPyo3.market_order( + trader_id=trader_id, + strategy_id=strategy_id, + instrument_id=AUDUSD_SIM, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(1), + ) + market_order_pyo3_dict = market_order_pyo3.to_dict() + market_order_cython = MarketOrder.from_pyo3(market_order_pyo3) + market_order_cython_dict = MarketOrder.to_dict(market_order_cython) + market_order_pyo3_back = nautilus_pyo3.MarketOrder.from_dict(market_order_cython_dict) + assert market_order_pyo3_dict == market_order_cython_dict + assert market_order_pyo3 == market_order_pyo3_back diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 6e205f73f8dc..eb47fb268745 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -384,6 +384,8 @@ def test_market_order_to_dict(self): # Act result = order.to_dict() + # remove init_id as it non-deterministic with order-factory + del result["init_id"] # Assert assert result == { @@ -405,7 +407,8 @@ def test_market_order_to_dict(self): "liquidity_side": "NO_LIQUIDITY_SIDE", "avg_px": None, "slippage": None, - "commissions": None, + "commissions": {}, + "emulation_trigger": "NO_TRIGGER", "status": "INITIALIZED", "contingency_type": "NO_CONTINGENCY", "order_list_id": None, From 8dc005eb81e6ca67e5740a1641ed9b713a2d1543 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Mar 2024 08:06:23 +1100 Subject: [PATCH 58/71] Improve LiquiditySide variant description --- nautilus_core/model/src/enums.rs | 4 ++-- nautilus_trader/core/includes/model.h | 2 +- nautilus_trader/core/rust/model.pxd | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index b7fb6383fba5..0917e05dd0e5 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -493,8 +493,8 @@ pub enum InstrumentCloseType { )] #[allow(clippy::enum_variant_names)] pub enum LiquiditySide { - /// No specific liqudity side. - NoLiquiditySide = 0, // Will be replaced by `Option` + /// No liquidity side specified. + NoLiquiditySide = 0, /// The order passively provided liqudity to the market to complete the trade (made a market). Maker = 1, /// The order aggressively took liqudity from the market to complete the trade. diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 37fb2d608f0a..ba453f410485 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -276,7 +276,7 @@ typedef enum InstrumentCloseType { */ typedef enum LiquiditySide { /** - * No specific liqudity side. + * No liquidity side specified. */ NO_LIQUIDITY_SIDE = 0, /** diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index b33e267e4729..a83b17530744 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -150,7 +150,7 @@ cdef extern from "../includes/model.h": # The liqudity side for a trade in a financial market. cpdef enum LiquiditySide: - # No specific liqudity side. + # No liquidity side specified. NO_LIQUIDITY_SIDE # = 0, # The order passively provided liqudity to the market to complete the trade (made a market). MAKER # = 1, From 22bcda3d8316465f20bf6cf2ad54887afed464a2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Mar 2024 08:07:11 +1100 Subject: [PATCH 59/71] Refine Rust market and limit orders --- nautilus_core/model/src/orders/limit.rs | 70 +++++++++---------- nautilus_core/model/src/orders/market.rs | 42 ++++++++++- .../model/src/python/orders/limit.rs | 20 +++++- .../model/src/python/orders/market.rs | 27 ++++--- 4 files changed, 111 insertions(+), 48 deletions(-) diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 0710b20556f3..36b9f6d1deda 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -371,41 +371,6 @@ impl Order for LimitOrder { } } -impl From for LimitOrder { - fn from(event: OrderInitialized) -> Self { - Self::new( - event.trader_id, - event.strategy_id, - event.instrument_id, - event.client_order_id, - event.order_side, - event.quantity, - event - .price // TODO: Improve this error, model order domain errors - .expect("Error initializing order: `price` was `None` for `LimitOrder"), - event.time_in_force, - event.expire_time, - event.post_only, - event.reduce_only, - event.quote_quantity, - event.display_qty, - event.emulation_trigger, - event.trigger_instrument_id, - event.contingency_type, - event.order_list_id, - event.linked_order_ids, - event.parent_order_id, - event.exec_algorithm_id, - event.exec_algorithm_params, - event.exec_spawn_id, - event.tags, - event.event_id, - event.ts_event, - ) - .unwrap() - } -} - impl Display for LimitOrder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -445,6 +410,41 @@ impl Display for LimitOrder { } } +impl From for LimitOrder { + fn from(event: OrderInitialized) -> Self { + Self::new( + event.trader_id, + event.strategy_id, + event.instrument_id, + event.client_order_id, + event.order_side, + event.quantity, + event + .price // TODO: Improve this error, model order domain errors + .expect("Error initializing order: `price` was `None` for `LimitOrder"), + event.time_in_force, + event.expire_time, + event.post_only, + event.reduce_only, + event.quote_quantity, + event.display_qty, + event.emulation_trigger, + event.trigger_instrument_id, + event.contingency_type, + event.order_list_id, + event.linked_order_ids, + event.parent_order_id, + event.exec_algorithm_id, + event.exec_algorithm_params, + event.exec_spawn_id, + event.tags, + event.event_id, + event.ts_event, + ) + .unwrap() + } +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 99a374ac23b3..dec7a10ef6b7 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -15,10 +15,12 @@ use std::{ collections::HashMap, + fmt::Display, ops::{Deref, DerefMut}, }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use serde::{Deserialize, Serialize}; use ustr::Ustr; use super::base::{Order, OrderCore}; @@ -41,7 +43,7 @@ use crate::{ }, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -338,6 +340,44 @@ impl Order for MarketOrder { } } +impl Display for MarketOrder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MarketOrder(\ + {} {} {} @ {} {}, \ + status={}, \ + client_order_id={}, \ + venue_order_id={}, \ + position_id={}, \ + exec_algorithm_id={}, \ + exec_spawn_id={}, \ + tags={:?}\ + )", + self.side, + self.quantity.to_formatted_string(), + self.instrument_id, + self.order_type, + self.time_in_force, + self.status, + self.client_order_id, + self.venue_order_id.map_or_else( + || "None".to_string(), + |venue_order_id| format!("{venue_order_id}") + ), + self.position_id.map_or_else( + || "None".to_string(), + |position_id| format!("{position_id}") + ), + self.exec_algorithm_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.exec_spawn_id + .map_or_else(|| "None".to_string(), |id| format!("{id}")), + self.tags + ) + } +} + impl From for MarketOrder { fn from(event: OrderInitialized) -> Self { Self::new( diff --git a/nautilus_core/model/src/python/orders/limit.rs b/nautilus_core/model/src/python/orders/limit.rs index 3e375e76d43d..65c735ab666b 100644 --- a/nautilus_core/model/src/python/orders/limit.rs +++ b/nautilus_core/model/src/python/orders/limit.rs @@ -25,7 +25,8 @@ use ustr::Ustr; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, + TimeInForce, TriggerType, }, identifiers::{ client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, @@ -33,7 +34,7 @@ use crate::{ trader_id::TraderId, }, orders::{ - base::{str_hashmap_to_ustr, Order}, + base::{str_hashmap_to_ustr, Order, OrderCore}, limit::LimitOrder, }, types::{price::Price, quantity::Quantity}, @@ -104,7 +105,8 @@ impl LimitOrder { fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { match op { CompareOp::Eq => self.eq(other).into_py(py), - _ => panic!("Not implemented"), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), } } @@ -360,6 +362,18 @@ impl LimitOrder { self.ts_init } + #[staticmethod] + #[pyo3(name = "opposite_side")] + fn py_opposite_side(side: OrderSide) -> OrderSide { + OrderCore::opposite_side(side) + } + + #[staticmethod] + #[pyo3(name = "closing_side")] + fn py_closing_side(side: PositionSide) -> OrderSide { + OrderCore::closing_side(side) + } + #[staticmethod] #[pyo3(name = "from_dict")] fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { diff --git a/nautilus_core/model/src/python/orders/market.rs b/nautilus_core/model/src/python/orders/market.rs index a9125561256a..968b9da11af0 100644 --- a/nautilus_core/model/src/python/orders/market.rs +++ b/nautilus_core/model/src/python/orders/market.rs @@ -91,20 +91,17 @@ impl MarketOrder { fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { match op { CompareOp::Eq => self.eq(other).into_py(py), - _ => panic!("Not implemented"), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), } } - #[staticmethod] - #[pyo3(name = "opposite_side")] - fn py_opposite_side(side: OrderSide) -> OrderSide { - OrderCore::opposite_side(side) + fn __str__(&self) -> String { + self.to_string() } - #[staticmethod] - #[pyo3(name = "closing_side")] - fn py_closing_side(side: PositionSide) -> OrderSide { - OrderCore::closing_side(side) + fn __repr__(&self) -> String { + self.to_string() } #[pyo3(name = "signed_decimal_qty")] @@ -263,6 +260,18 @@ impl MarketOrder { self.tags.map(|x| x.to_string()) } + #[staticmethod] + #[pyo3(name = "opposite_side")] + fn py_opposite_side(side: OrderSide) -> OrderSide { + OrderCore::opposite_side(side) + } + + #[staticmethod] + #[pyo3(name = "closing_side")] + fn py_closing_side(side: PositionSide) -> OrderSide { + OrderCore::closing_side(side) + } + #[pyo3(name = "to_dict")] fn py_to_dict(&self, py: Python<'_>) -> PyResult { let dict = PyDict::new(py); From 88199d10f03076211af83c146a9563d00dccc7aa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Mar 2024 19:05:37 +1100 Subject: [PATCH 60/71] Update dependencies including reqwest --- nautilus_core/Cargo.lock | 12 ++++++------ nautilus_core/network/Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index bbc6b5244017..72c3f23784f7 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -3562,9 +3562,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.26" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64", "bytes", @@ -3760,9 +3760,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ "bitflags 2.5.0", "errno", diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index 78f574555f68..ef7832fbf1a7 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -23,7 +23,7 @@ futures-util = "0.3.30" http = "1.1.0" hyper = "1.2.0" nonzero_ext = "0.3.0" -reqwest = "0.11.26" +reqwest = "0.11.27" tokio-tungstenite = { path = "./tokio-tungstenite", features = ["rustls-tls-native-roots"] } [dev-dependencies] From 2d838907bb386884cab332611c2ebc6688b76b40 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Mar 2024 20:46:46 +1100 Subject: [PATCH 61/71] Redact Redis password in strings and logs --- RELEASES.md | 1 + nautilus_core/common/src/redis.rs | 59 +++++++++++++++++++------- nautilus_trader/common/config.py | 19 +++++++++ tests/unit_tests/config/test_common.py | 13 +++++- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 1aa0fab7830e..f13d6d608f36 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,7 @@ Released on TBD (UTC). - Improved Redis cache adapter and message bus error handling and logging - Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton - Refactored `InteractiveBrokersEWrapper`, thanks @rsmb7z +- Redact Redis passwords in logs - Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies ### Breaking Changes diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index d4c77e911409..c0b9be0caa23 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -142,7 +142,7 @@ fn drain_buffer( pipe.query::<()>(conn).map_err(anyhow::Error::from) } -pub fn get_redis_url(database_config: &serde_json::Value) -> String { +pub fn get_redis_url(database_config: &serde_json::Value) -> (String, String) { let host = database_config .get("host") .and_then(|v| v.as_str()) @@ -164,24 +164,46 @@ pub fn get_redis_url(database_config: &serde_json::Value) -> String { .and_then(|v| v.as_bool()) .unwrap_or(false); + let redacted_password = if password.len() > 4 { + format!("{}...{}", &password[..2], &password[password.len() - 2..],) + } else { + password.to_string() + }; + let auth_part = if !username.is_empty() && !password.is_empty() { format!("{}:{}@", username, password) } else { String::new() }; - format!( + let redacted_auth_part = if !username.is_empty() && !password.is_empty() { + format!("{}:{}@", username, redacted_password) + } else { + String::new() + }; + + let url = format!( "redis{}://{}{}:{}", if use_ssl { "s" } else { "" }, auth_part, host, port - ) + ); + + let redacted_url = format!( + "redis{}://{}{}:{}", + if use_ssl { "s" } else { "" }, + redacted_auth_part, + host, + port + ); + + (url, redacted_url) } pub fn create_redis_connection(database_config: &serde_json::Value) -> RedisResult { - let redis_url = get_redis_url(database_config); - debug!("Connecting to {redis_url}"); + let (redis_url, redacted_url) = get_redis_url(database_config); + debug!("Connecting to {redacted_url}"); let default_timeout = 20; let timeout = get_timeout_duration(database_config, default_timeout); let client = redis::Client::open(redis_url)?; @@ -248,8 +270,9 @@ mod tests { #[rstest] fn test_get_redis_url_default_values() { let config = json!({}); - let url = get_redis_url(&config); + let (url, redacted_url) = get_redis_url(&config); assert_eq!(url, "redis://127.0.0.1:6379"); + assert_eq!(redacted_url, "redis://127.0.0.1:6379"); } #[rstest] @@ -261,8 +284,9 @@ mod tests { "password": "pass", "ssl": true, }); - let url = get_redis_url(&config); + let (url, redacted_url) = get_redis_url(&config); assert_eq!(url, "rediss://user:pass@example.com:6380"); + assert_eq!(redacted_url, "rediss://user:pass@example.com:6380"); } #[rstest] @@ -270,12 +294,13 @@ mod tests { let config = json!({ "host": "example.com", "port": 6380, - "username": "user", - "password": "pass", + "username": "username", + "password": "password", "ssl": false, }); - let url = get_redis_url(&config); - assert_eq!(url, "redis://user:pass@example.com:6380"); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://username:password@example.com:6380"); + assert_eq!(redacted_url, "redis://username:pa...rd@example.com:6380"); } #[rstest] @@ -285,8 +310,9 @@ mod tests { "port": 6380, "ssl": false, }); - let url = get_redis_url(&config); + let (url, redacted_url) = get_redis_url(&config); assert_eq!(url, "redis://example.com:6380"); + assert_eq!(redacted_url, "redis://example.com:6380"); } #[rstest] @@ -294,12 +320,13 @@ mod tests { let config = json!({ "host": "example.com", "port": 6380, - "username": "user", - "password": "pass", + "username": "username", + "password": "password", // "ssl" is intentionally omitted to test default behavior }); - let url = get_redis_url(&config); - assert_eq!(url, "redis://user:pass@example.com:6380"); + let (url, redacted_url) = get_redis_url(&config); + assert_eq!(url, "redis://username:password@example.com:6380"); + assert_eq!(redacted_url, "redis://username:pa...rd@example.com:6380"); } #[rstest] diff --git a/nautilus_trader/common/config.py b/nautilus_trader/common/config.py index ecb386b00cb0..df7bc5a2cf05 100644 --- a/nautilus_trader/common/config.py +++ b/nautilus_trader/common/config.py @@ -247,6 +247,7 @@ class DatabaseConfig(NautilusConfig, frozen=True): The account username for the database connection. password : str, optional The account password for the database connection. + If a value is provided then it will be redacted in the string repr for this object. ssl : bool, default False If database should use an SSL enabled connection. timeout : int, default 20 @@ -266,6 +267,24 @@ class DatabaseConfig(NautilusConfig, frozen=True): ssl: bool = False timeout: int | None = 20 + def __repr__(self) -> str: + redacted_password = "None" + if self.password: + if len(self.password) >= 4: + redacted_password = f"{self.password[:2]}...{self.password[-2:]}" + else: + redacted_password = self.password + return ( + f"{type(self).__name__}(" + f"type={self.type}, " + f"host={self.host}, " + f"port={self.port}, " + f"username={self.username}, " + f"password={redacted_password}, " + f"ssl={self.ssl}, " + f"timeout={self.timeout})" + ) + class MessageBusConfig(NautilusConfig, frozen=True): """ diff --git a/tests/unit_tests/config/test_common.py b/tests/unit_tests/config/test_common.py index 622e85080d3c..0afb6582022c 100644 --- a/tests/unit_tests/config/test_common.py +++ b/tests/unit_tests/config/test_common.py @@ -40,6 +40,17 @@ from nautilus_trader.test_kit.providers import TestInstrumentProvider +def test_repr_with_redacted_password() -> None: + # Arrange + config = DatabaseConfig(username="username", password="password") + + # Act, Assert + assert ( + repr(config) + == "DatabaseConfig(type=redis, host=None, port=None, username=username, password=pa...rd, ssl=False, timeout=20)" + ) + + def test_equality_hash_repr() -> None: # Arrange config1 = DatabaseConfig() @@ -51,7 +62,7 @@ def test_equality_hash_repr() -> None: assert isinstance(hash(config1), int) assert ( repr(config1) - == "DatabaseConfig(type='redis', host=None, port=None, username=None, password=None, ssl=False, timeout=20)" + == "DatabaseConfig(type=redis, host=None, port=None, username=None, password=None, ssl=False, timeout=20)" ) From 8642500b204b2d6e3f97ba131011b123cc06e43c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Mar 2024 22:35:54 +1100 Subject: [PATCH 62/71] Improve Redis port parsing --- RELEASES.md | 3 ++- nautilus_core/common/src/redis.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f13d6d608f36..3c2867884ce2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,8 +9,9 @@ Released on TBD (UTC). - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging - Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton +- Improved Redis port parsing (`DatabaseConfig.port` can now be either a string or integer) +- Redact Redis passwords in strings and logs - Refactored `InteractiveBrokersEWrapper`, thanks @rsmb7z -- Redact Redis passwords in logs - Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies ### Breaking Changes diff --git a/nautilus_core/common/src/redis.rs b/nautilus_core/common/src/redis.rs index c0b9be0caa23..99cbfb730bac 100644 --- a/nautilus_core/common/src/redis.rs +++ b/nautilus_core/common/src/redis.rs @@ -149,7 +149,10 @@ pub fn get_redis_url(database_config: &serde_json::Value) -> (String, String) { .unwrap_or("127.0.0.1"); let port = database_config .get("port") - .and_then(|v| v.as_u64()) + .and_then(|v| { + v.as_u64() + .or_else(|| v.as_str().and_then(|s| s.parse().ok())) + }) .unwrap_or(6379); let username = database_config .get("username") From 63dabdd213d576361730dec3299d08ad75eff6ed Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Wed, 20 Mar 2024 20:18:01 -0400 Subject: [PATCH 63/71] Fix Interactive Brokers client start (#1551) --- .../interactive_brokers/client/client.py | 4 +- .../interactive_brokers/client/connection.py | 9 +- .../interactive_brokers/client/wrapper.py | 187 +++++++++--------- .../adapters/interactive_brokers/factories.py | 1 + 4 files changed, 106 insertions(+), 95 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index cbe5dd7fa2e1..4d1550cd6e43 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -526,7 +526,7 @@ async def _run_internal_msg_queue_processor(self) -> None: Continuously process messages from the internal incoming message queue. """ self._log.debug( - "Client internal message queue started.", + "Client internal message queue processor started.", ) try: while ( @@ -548,7 +548,7 @@ async def _run_internal_msg_queue_processor(self) -> None: ) ) finally: - self._log.debug("Client TWS incoming message reader stopped.") + self._log.debug("Internal message queue processor stopped.") def _process_message(self, msg: str) -> bool: """ diff --git a/nautilus_trader/adapters/interactive_brokers/client/connection.py b/nautilus_trader/adapters/interactive_brokers/client/connection.py index a009bff75239..f76d1852f9a0 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/connection.py +++ b/nautilus_trader/adapters/interactive_brokers/client/connection.py @@ -106,10 +106,11 @@ async def _handle_reconnect(self) -> None: ) await asyncio.sleep(self._reconnect_delay) await self._startup() - await self._resubscribe_all() # should this not be done in _resume? - self._resume() - else: - self._reconnect_attempts = 0 + + self._log.info("Reconnection successful.") + self._reconnect_attempts = 0 + await self._resubscribe_all() + self._resume() def _initialize_connection_params(self) -> None: """ diff --git a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py index 7e662c331dcd..eac080a9a252 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/wrapper.py +++ b/nautilus_trader/adapters/interactive_brokers/client/wrapper.py @@ -64,7 +64,7 @@ def __init__( self._log = nautilus_logger self._client = client - def logAnswer(self, fnName, fnParams): + def logAnswer(self, fnName, fnParams) -> None: """ Override the logging for EWrapper.logAnswer. """ @@ -81,7 +81,7 @@ def error( errorCode: int, errorString: str, advancedOrderRejectJson="", - ): + ) -> None: """ Call this event in response to an error in communication or when TWS needs to send a message to the client. @@ -94,16 +94,16 @@ def error( advanced_order_reject_json=advancedOrderRejectJson, ) - def winError(self, text: str, lastError: int): + def winError(self, text: str, lastError: int) -> None: self.logAnswer(current_fn_name(), vars()) - def connectAck(self): + def connectAck(self) -> None: """ Invoke this callback to signify the completion of a successful connection. """ self.logAnswer(current_fn_name(), vars()) - def marketDataType(self, reqId: TickerId, marketDataType: int): + def marketDataType(self, reqId: TickerId, marketDataType: int) -> None: """ Receives notification when the market data type changes. @@ -131,7 +131,7 @@ def tickPrice( tickType: TickType, price: float, attrib: TickAttrib, - ): + ) -> None: """ Market data tick price callback. @@ -149,7 +149,7 @@ def tickPrice( """ self.logAnswer(current_fn_name(), vars()) - def tickSize(self, reqId: TickerId, tickType: TickType, size: Decimal): + def tickSize(self, reqId: TickerId, tickType: TickType, size: Decimal) -> None: """ Handle tick size-related market data. @@ -168,17 +168,17 @@ def tickSize(self, reqId: TickerId, tickType: TickType, size: Decimal): """ self.logAnswer(current_fn_name(), vars()) - def tickSnapshotEnd(self, reqId: int): + def tickSnapshotEnd(self, reqId: int) -> None: """ When requesting market data snapshots, this market will indicate the snapshot reception is finished. """ self.logAnswer(current_fn_name(), vars()) - def tickGeneric(self, reqId: TickerId, tickType: TickType, value: float): + def tickGeneric(self, reqId: TickerId, tickType: TickType, value: float) -> None: self.logAnswer(current_fn_name(), vars()) - def tickString(self, reqId: TickerId, tickType: TickType, value: str): + def tickString(self, reqId: TickerId, tickType: TickType, value: str) -> None: self.logAnswer(current_fn_name(), vars()) def tickEFP( @@ -192,7 +192,7 @@ def tickEFP( futureLastTradeDate: str, dividendImpact: float, dividendsToLastTradeDate: float, - ): + ) -> None: """ Market data callback for Exchange for Physical. @@ -234,7 +234,7 @@ def orderStatus( clientId: int, whyHeld: str, mktCapPrice: float, - ): + ) -> None: """ Call this event whenever the status of an order changes. Also, fire it after reconnecting to TWS if the client has any open orders. @@ -288,7 +288,7 @@ def openOrder( contract: Contract, order: Order, orderState: OrderState, - ): + ) -> None: """ Call this function to feed in open orders. @@ -312,14 +312,14 @@ def openOrder( order_state=orderState, ) - def openOrderEnd(self): + def openOrderEnd(self) -> None: """ Call this at the end of a given request for open orders. """ self.logAnswer(current_fn_name(), vars()) self._client.process_open_order_end() - def connectionClosed(self): + def connectionClosed(self) -> None: """ Call this function when TWS closes the socket connection with the ActiveX control, or when TWS is shut down. @@ -333,7 +333,7 @@ def updateAccountValue( val: str, currency: str, accountName: str, - ): + ) -> None: """ Call this function only when ReqAccountUpdates on EEClientSocket object has been called. @@ -350,30 +350,30 @@ def updatePortfolio( unrealizedPNL: float, realizedPNL: float, accountName: str, - ): + ) -> None: """ Call this function only when reqAccountUpdates on EEClientSocket object has been called. """ self.logAnswer(current_fn_name(), vars()) - def updateAccountTime(self, timeStamp: str): + def updateAccountTime(self, timeStamp: str) -> None: self.logAnswer(current_fn_name(), vars()) - def accountDownloadEnd(self, accountName: str): + def accountDownloadEnd(self, accountName: str) -> None: """ Call this after a batch updateAccountValue() and updatePortfolio() is sent. """ self.logAnswer(current_fn_name(), vars()) - def nextValidId(self, orderId: int): + def nextValidId(self, orderId: int) -> None: """ Receives next valid order id. """ self.logAnswer(current_fn_name(), vars()) self._client.process_next_valid_id(order_id=orderId) - def contractDetails(self, reqId: int, contractDetails: ContractDetails): + def contractDetails(self, reqId: int, contractDetails: ContractDetails) -> None: """ Receives the full contract's definitions. @@ -385,14 +385,14 @@ def contractDetails(self, reqId: int, contractDetails: ContractDetails): self.logAnswer(current_fn_name(), vars()) self._client.process_contract_details(req_id=reqId, contract_details=contractDetails) - def bondContractDetails(self, reqId: int, contractDetails: ContractDetails): + def bondContractDetails(self, reqId: int, contractDetails: ContractDetails) -> None: """ Call this function when the reqContractDetails function has been called for bonds. """ self.logAnswer(current_fn_name(), vars()) - def contractDetailsEnd(self, reqId: int): + def contractDetailsEnd(self, reqId: int) -> None: """ Call this function once all contract details for a given request are received. @@ -402,7 +402,7 @@ def contractDetailsEnd(self, reqId: int): self.logAnswer(current_fn_name(), vars()) self._client.process_contract_details_end(req_id=reqId) - def execDetails(self, reqId: int, contract: Contract, execution: Execution): + def execDetails(self, reqId: int, contract: Contract, execution: Execution) -> None: """ Fire this event when the reqExecutions() function is invoked or when an order is filled. @@ -414,7 +414,7 @@ def execDetails(self, reqId: int, contract: Contract, execution: Execution): execution=execution, ) - def execDetailsEnd(self, reqId: int): + def execDetailsEnd(self, reqId: int) -> None: """ Call this function once all executions have been sent to a client in response to reqExecutions(). @@ -429,7 +429,7 @@ def updateMktDepth( side: int, price: float, size: Decimal, - ): + ) -> None: """ Return the order book. @@ -464,7 +464,7 @@ def updateMktDepthL2( price: float, size: Decimal, isSmartDepth: bool, - ): + ) -> None: """ Return the order book. @@ -499,7 +499,7 @@ def updateNewsBulletin( msgType: int, newsMessage: str, originExch: str, - ): + ) -> None: """ Provide IB's bulletins. @@ -520,14 +520,14 @@ def updateNewsBulletin( """ self.logAnswer(current_fn_name(), vars()) - def managedAccounts(self, accountsList: str): + def managedAccounts(self, accountsList: str) -> None: """ Receives a comma-separated string with the managed account ids. """ self.logAnswer(current_fn_name(), vars()) self._client.process_managed_accounts(accounts_list=accountsList) - def receiveFA(self, faData: FaDataType, cxml: str): + def receiveFA(self, faData: FaDataType, cxml: str) -> None: """ Receives the Financial Advisor's configuration available in the TWS. @@ -545,7 +545,7 @@ def receiveFA(self, faData: FaDataType, cxml: str): """ self.logAnswer(current_fn_name(), vars()) - def historicalData(self, reqId: int, bar: BarData): + def historicalData(self, reqId: int, bar: BarData) -> None: """ Return the requested historical data bars. @@ -559,14 +559,14 @@ def historicalData(self, reqId: int, bar: BarData): """ self.logAnswer(current_fn_name(), vars()) - def historicalDataEnd(self, reqId: int, start: str, end: str): + def historicalDataEnd(self, reqId: int, start: str, end: str) -> None: """ Mark the end of the reception of historical bars. """ self.logAnswer(current_fn_name(), vars()) self._client.process_historical_data_end(req_id=reqId, start=start, end=end) - def scannerParameters(self, xml: str): + def scannerParameters(self, xml: str) -> None: """ Provide the XML-formatted parameters available to create a market scanner. @@ -587,7 +587,7 @@ def scannerData( benchmark: str, projection: str, legsStr: str, - ): + ) -> None: """ Provide the data resulting from the market scanner request. @@ -611,7 +611,7 @@ def scannerData( """ self.logAnswer(current_fn_name(), vars()) - def scannerDataEnd(self, reqId: int): + def scannerDataEnd(self, reqId: int) -> None: """ Indicate that scanner data reception has terminated. @@ -634,7 +634,7 @@ def realtimeBar( volume: Decimal, wap: Decimal, count: int, - ): + ) -> None: """ Update real-time 5-second bars. @@ -673,14 +673,14 @@ def realtimeBar( count=count, ) - def currentTime(self, time: int): + def currentTime(self, time: int) -> None: """ Obtain the IB server's system time by calling this method as a result of invoking `reqCurrentTime`. """ self.logAnswer(current_fn_name(), vars()) - def fundamentalData(self, reqId: TickerId, data: str): + def fundamentalData(self, reqId: TickerId, data: str) -> None: """ Call this function to receive fundamental market data. @@ -690,7 +690,11 @@ def fundamentalData(self, reqId: TickerId, data: str): """ self.logAnswer(current_fn_name(), vars()) - def deltaNeutralValidation(self, reqId: int, deltaNeutralContract: DeltaNeutralContract): + def deltaNeutralValidation( + self, + reqId: int, + deltaNeutralContract: DeltaNeutralContract, + ) -> None: """ When accepting a Delta-Neutral RFQ (request for quote), the server sends a deltaNeutralValidation() message with the DeltaNeutralContract structure. @@ -702,7 +706,7 @@ def deltaNeutralValidation(self, reqId: int, deltaNeutralContract: DeltaNeutralC """ self.logAnswer(current_fn_name(), vars()) - def commissionReport(self, commissionReport: CommissionReport): + def commissionReport(self, commissionReport: CommissionReport) -> None: """ Trigger this callback in the following scenarios: @@ -719,7 +723,7 @@ def position( contract: Contract, position: Decimal, avgCost: float, - ): + ) -> None: """ Return real-time positions for all accounts in response to the reqPositions() method. @@ -732,7 +736,7 @@ def position( avg_cost=avgCost, ) - def positionEnd(self): + def positionEnd(self) -> None: """ Call this once all position data for a given request has been received, serving as an end marker for the position() data. @@ -747,7 +751,7 @@ def accountSummary( tag: str, value: str, currency: str, - ): + ) -> None: """ Return the data from the TWS Account Window Summary tab in response to reqAccountSummary(). @@ -761,26 +765,26 @@ def accountSummary( currency=currency, ) - def accountSummaryEnd(self, reqId: int): + def accountSummaryEnd(self, reqId: int) -> None: """ Call this method when all account summary data for a given request has been received. """ self.logAnswer(current_fn_name(), vars()) - def verifyCompleted(self, isSuccessful: bool, errorText: str): + def verifyCompleted(self, isSuccessful: bool, errorText: str) -> None: self.logAnswer(current_fn_name(), vars()) - def verifyAndAuthMessageAPI(self, apiData: str, xyzChallange: str): + def verifyAndAuthMessageAPI(self, apiData: str, xyzChallange: str) -> None: self.logAnswer(current_fn_name(), vars()) - def verifyAndAuthCompleted(self, isSuccessful: bool, errorText: str): + def verifyAndAuthCompleted(self, isSuccessful: bool, errorText: str) -> None: self.logAnswer(current_fn_name(), vars()) - def displayGroupList(self, reqId: int, groups: str): + def displayGroupList(self, reqId: int, groups: str) -> None: """ Receive a one-time response callback to queryDisplayGroups(). @@ -796,7 +800,7 @@ def displayGroupList(self, reqId: int, groups: str): """ self.logAnswer(current_fn_name(), vars()) - def displayGroupUpdated(self, reqId: int, contractInfo: str): + def displayGroupUpdated(self, reqId: int, contractInfo: str) -> None: """ Receive a notification from TWS to the API client after subscribing to group events via subscribeToGroupEvents(). This notification will be resent if the @@ -824,14 +828,14 @@ def positionMulti( contract: Contract, pos: Decimal, avgCost: float, - ): + ) -> None: """ Retrieve the position for a specific account or model, mirroring the position() function. """ self.logAnswer(current_fn_name(), vars()) - def positionMultiEnd(self, reqId: int): + def positionMultiEnd(self, reqId: int) -> None: """ Terminate the position for a specific account or model, akin to the positionEnd() function. @@ -846,14 +850,14 @@ def accountUpdateMulti( key: str, value: str, currency: str, - ): + ) -> None: """ Update the value for a specific account or model, similar to the updateAccountValue() function. """ self.logAnswer(current_fn_name(), vars()) - def accountUpdateMultiEnd(self, reqId: int): + def accountUpdateMultiEnd(self, reqId: int) -> None: """ Download data for a specific account or model, resembling accountDownloadEnd() functionality. @@ -873,7 +877,7 @@ def tickOptionComputation( vega: float, theta: float, undPrice: float, - ): + ) -> None: """ Invoke this function in response to market movements in an option or its underlier. @@ -893,7 +897,7 @@ def securityDefinitionOptionParameter( multiplier: str, expirations: SetOfString, strikes: SetOfFloat, - ): + ) -> None: """ Return the option chain for an underlying on a specified exchange. @@ -929,7 +933,7 @@ def securityDefinitionOptionParameter( strikes=strikes, ) - def securityDefinitionOptionParameterEnd(self, reqId: int): + def securityDefinitionOptionParameterEnd(self, reqId: int) -> None: """ Invoke this after all callbacks to securityDefinitionOptionParameter have been completed. @@ -943,7 +947,7 @@ def securityDefinitionOptionParameterEnd(self, reqId: int): self.logAnswer(current_fn_name(), vars()) self._client.process_security_definition_option_parameter_end(req_id=reqId) - def softDollarTiers(self, reqId: int, tiers: list): + def softDollarTiers(self, reqId: int, tiers: list) -> None: """ Invoke this upon receiving Soft Dollar Tier configuration information. @@ -959,7 +963,7 @@ def softDollarTiers(self, reqId: int, tiers: list): """ self.logAnswer(current_fn_name(), vars()) - def familyCodes(self, familyCodes: ListOfFamilyCode): + def familyCodes(self, familyCodes: ListOfFamilyCode) -> None: """ Return an array of family codes. """ @@ -969,7 +973,7 @@ def symbolSamples( self, reqId: int, contractDescriptions: ListOfContractDescription, - ): + ) -> None: """ Return an array of sample contract descriptions. """ @@ -979,7 +983,7 @@ def symbolSamples( contract_descriptions=contractDescriptions, ) - def mktDepthExchanges(self, depthMktDataDescriptions: ListOfDepthExchanges): + def mktDepthExchanges(self, depthMktDataDescriptions: ListOfDepthExchanges) -> None: """ Return an array of exchanges that provide depth data to UpdateMktDepthL2. """ @@ -993,13 +997,13 @@ def tickNews( articleId: str, headline: str, extraData: str, - ): + ) -> None: """ Return news headlines. """ self.logAnswer(current_fn_name(), vars()) - def smartComponents(self, reqId: int, smartComponentMap: SmartComponentMap): + def smartComponents(self, reqId: int, smartComponentMap: SmartComponentMap) -> None: """ Return exchange component mapping. """ @@ -1011,19 +1015,19 @@ def tickReqParams( minTick: float, bboExchange: str, snapshotPermissions: int, - ): + ) -> None: """ Return the exchange map for a specific contract. """ self.logAnswer(current_fn_name(), vars()) - def newsProviders(self, newsProviders: ListOfNewsProviders): + def newsProviders(self, newsProviders: ListOfNewsProviders) -> None: """ Return available and subscribed API news providers. """ self.logAnswer(current_fn_name(), vars()) - def newsArticle(self, requestId: int, articleType: int, articleText: str): + def newsArticle(self, requestId: int, articleType: int, articleText: str) -> None: """ Return the body of a news article. """ @@ -1036,32 +1040,32 @@ def historicalNews( providerCode: str, articleId: str, headline: str, - ): + ) -> None: """ Return historical news headlines. """ self.logAnswer(current_fn_name(), vars()) - def historicalNewsEnd(self, requestId: int, hasMore: bool): + def historicalNewsEnd(self, requestId: int, hasMore: bool) -> None: """ Signals end of historical news. """ self.logAnswer(current_fn_name(), vars()) - def headTimestamp(self, reqId: int, headTimestamp: str): + def headTimestamp(self, reqId: int, headTimestamp: str) -> None: """ Return the earliest available data for a specific type of data for a given contract. """ self.logAnswer(current_fn_name(), vars()) - def histogramData(self, reqId: int, items: HistogramData): + def histogramData(self, reqId: int, items: HistogramData) -> None: """ Return histogram data for a contract. """ self.logAnswer(current_fn_name(), vars()) - def historicalDataUpdate(self, reqId: int, bar: BarData): + def historicalDataUpdate(self, reqId: int, bar: BarData) -> None: """ Return updates in real time when keepUpToDate is set to True. """ @@ -1071,25 +1075,25 @@ def historicalDataUpdate(self, reqId: int, bar: BarData): bar=bar, ) - def rerouteMktDataReq(self, reqId: int, conId: int, exchange: str): + def rerouteMktDataReq(self, reqId: int, conId: int, exchange: str) -> None: """ Return rerouted CFD contract information for a market data request. """ self.logAnswer(current_fn_name(), vars()) - def rerouteMktDepthReq(self, reqId: int, conId: int, exchange: str): + def rerouteMktDepthReq(self, reqId: int, conId: int, exchange: str) -> None: """ Return rerouted CFD contract information for a market depth request. """ self.logAnswer(current_fn_name(), vars()) - def marketRule(self, marketRuleId: int, priceIncrements: ListOfPriceIncrements): + def marketRule(self, marketRuleId: int, priceIncrements: ListOfPriceIncrements) -> None: """ Return the minimum price increment structure for a specific market rule ID. """ self.logAnswer(current_fn_name(), vars()) - def pnl(self, reqId: int, dailyPnL: float, unrealizedPnL: float, realizedPnL: float): + def pnl(self, reqId: int, dailyPnL: float, unrealizedPnL: float, realizedPnL: float) -> None: """ Return the daily Profit and Loss (PnL) for the account. """ @@ -1103,13 +1107,13 @@ def pnlSingle( unrealizedPnL: float, realizedPnL: float, value: float, - ): + ) -> None: """ Return the daily Profit and Loss (PnL) for a single position in the account. """ self.logAnswer(current_fn_name(), vars()) - def historicalTicks(self, reqId: int, ticks: ListOfHistoricalTick, done: bool): + def historicalTicks(self, reqId: int, ticks: ListOfHistoricalTick, done: bool) -> None: """ Return historical tick data when whatToShow is set to MIDPOINT. """ @@ -1120,7 +1124,12 @@ def historicalTicks(self, reqId: int, ticks: ListOfHistoricalTick, done: bool): done=done, ) - def historicalTicksBidAsk(self, reqId: int, ticks: ListOfHistoricalTickBidAsk, done: bool): + def historicalTicksBidAsk( + self, + reqId: int, + ticks: ListOfHistoricalTickBidAsk, + done: bool, + ) -> None: """ Return historical tick data when whatToShow is set to BID_ASK. """ @@ -1131,7 +1140,7 @@ def historicalTicksBidAsk(self, reqId: int, ticks: ListOfHistoricalTickBidAsk, d done=done, ) - def historicalTicksLast(self, reqId: int, ticks: ListOfHistoricalTickLast, done: bool): + def historicalTicksLast(self, reqId: int, ticks: ListOfHistoricalTickLast, done: bool) -> None: """ Return historical tick data when whatToShow is set to TRADES. """ @@ -1152,7 +1161,7 @@ def tickByTickAllLast( tickAttribLast: TickAttribLast, exchange: str, specialConditions: str, - ): + ) -> None: """ Return tick-by-tick data for tickType set to "Last" or "AllLast". """ @@ -1177,7 +1186,7 @@ def tickByTickBidAsk( bidSize: Decimal, askSize: Decimal, tickAttribBidAsk: TickAttribBidAsk, - ): + ) -> None: """ Return tick-by-tick data for tickType set to "BidAsk". """ @@ -1192,19 +1201,19 @@ def tickByTickBidAsk( tick_attrib_bid_ask=tickAttribBidAsk, ) - def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float): + def tickByTickMidPoint(self, reqId: int, time: int, midPoint: float) -> None: """ Return tick-by-tick data for tickType set to "MidPoint". """ self.logAnswer(current_fn_name(), vars()) - def orderBound(self, reqId: int, apiClientId: int, apiOrderId: int): + def orderBound(self, reqId: int, apiClientId: int, apiOrderId: int) -> None: """ Return the orderBound notification. """ self.logAnswer(current_fn_name(), vars()) - def completedOrder(self, contract: Contract, order: Order, orderState: OrderState): + def completedOrder(self, contract: Contract, order: Order, orderState: OrderState) -> None: """ Feed in completed orders. @@ -1222,22 +1231,22 @@ def completedOrder(self, contract: Contract, order: Order, orderState: OrderStat """ self.logAnswer(current_fn_name(), vars()) - def completedOrdersEnd(self): + def completedOrdersEnd(self) -> None: """ Invoke this upon completing a request for completed orders. """ self.logAnswer(current_fn_name(), vars()) - def replaceFAEnd(self, reqId: int, text: str): + def replaceFAEnd(self, reqId: int, text: str) -> None: """ Invoke this at the completion of a Financial Advisor (FA) replacement operation. """ self.logAnswer(current_fn_name(), vars()) - def wshMetaData(self, reqId: int, dataJson: str): + def wshMetaData(self, reqId: int, dataJson: str) -> None: self.logAnswer(current_fn_name(), vars()) - def wshEventData(self, reqId: int, dataJson: str): + def wshEventData(self, reqId: int, dataJson: str) -> None: self.logAnswer(current_fn_name(), vars()) def historicalSchedule( @@ -1247,13 +1256,13 @@ def historicalSchedule( endDateTime: str, timeZone: str, sessions: ListOfHistoricalSessions, - ): + ) -> None: """ Return historical schedule for historical data request with whatToShow=SCHEDULE. """ self.logAnswer(current_fn_name(), vars()) - def userInfo(self, reqId: int, whiteBrandingId: str): + def userInfo(self, reqId: int, whiteBrandingId: str) -> None: """ Return user info. """ diff --git a/nautilus_trader/adapters/interactive_brokers/factories.py b/nautilus_trader/adapters/interactive_brokers/factories.py index 66098b03fa71..120460855c58 100644 --- a/nautilus_trader/adapters/interactive_brokers/factories.py +++ b/nautilus_trader/adapters/interactive_brokers/factories.py @@ -103,6 +103,7 @@ def get_cached_ib_client( port=port, client_id=client_id, ) + client.start() IB_CLIENTS[client_key] = client return IB_CLIENTS[client_key] From 604aeacf702785928f28f6f01a65af843ef0c7ea Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 06:51:24 +1100 Subject: [PATCH 64/71] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 4 ++-- poetry.lock | 46 ++++++++++++++++++++-------------------- pyproject.toml | 4 ++-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 344d787d4b12..57a9a3751522 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,7 +82,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.3.4 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 72c3f23784f7..255b2452ae5e 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4043,9 +4043,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" diff --git a/poetry.lock b/poetry.lock index 22a2d174c97f..471d16086719 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1760,17 +1760,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.13.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.13.0.tar.gz", hash = "sha256:58c73e7eae1fc05aebe7bd4b3b59eb5beb5c0b644c7e2dd3cc652a749f0056e3"}, + {file = "pytest_mock-3.13.0-py3-none-any.whl", hash = "sha256:158462fe2124763df267f20c81f098bdccf8928e2649e5e1093955ad1e515f80"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] @@ -1945,28 +1945,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.3.3" +version = "0.3.4" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, - {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, - {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, - {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, - {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, + {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, + {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, + {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, + {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, ] [[package]] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "a9178495b3efb2556925ccdd78e19f202495df9caebf254bcc5d336c7e424b6e" +content-hash = "8c0e6f05e530ec3be9f6fde0ff69a9ca769a5ca60e0de65d79d2b3b385af5053" diff --git a/pyproject.toml b/pyproject.toml index da3bb8ac337f..c1ea1a13557a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ docformatter = "^1.7.5" mypy = "^1.9.0" pandas-stubs = "^2.2.1" pre-commit = "^3.6.2" -ruff = "^0.3.3" +ruff = "^0.3.4" types-pytz = "^2023.3" types-requests = "^2.31" types-toml = "^0.10.2" @@ -98,7 +98,7 @@ pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" -pytest-mock = "^3.12.0" +pytest-mock = "^3.13.0" pytest-xdist = { version = "^3.5.0", extras = ["psutil"] } [tool.poetry.group.docs] From 387cbdb23968b05ecd9926917137e9d2c1f2c1eb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 06:53:13 +1100 Subject: [PATCH 65/71] Update Databento publishers --- .../adapters/src/databento/publishers.json | 114 ++++++++++++++++++ .../adapters/databento/publishers.json | 114 ++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/nautilus_core/adapters/src/databento/publishers.json b/nautilus_core/adapters/src/databento/publishers.json index f9f45ff0c837..cbd2492e6c9b 100644 --- a/nautilus_core/adapters/src/databento/publishers.json +++ b/nautilus_core/adapters/src/databento/publishers.json @@ -364,5 +364,119 @@ "dataset": "OPRA.PILLAR", "venue": "SPHR", "description": "OPRA - MIAX Sapphire" + }, + { + "publisher_id": 62, + "dataset": "DBEQ.MAX", + "venue": "XCHI", + "description": "DBEQ Max - NYSE Chicago" + }, + { + "publisher_id": 63, + "dataset": "DBEQ.MAX", + "venue": "XCIS", + "description": "DBEQ Max - NYSE National" + }, + { + "publisher_id": 64, + "dataset": "DBEQ.MAX", + "venue": "IEXG", + "description": "DBEQ Max - IEX" + }, + { + "publisher_id": 65, + "dataset": "DBEQ.MAX", + "venue": "EPRL", + "description": "DBEQ Max - MIAX Pearl" + }, + { + "publisher_id": 66, + "dataset": "DBEQ.MAX", + "venue": "XNAS", + "description": "DBEQ Max - Nasdaq" + }, + { + "publisher_id": 67, + "dataset": "DBEQ.MAX", + "venue": "XNYS", + "description": "DBEQ Max - NYSE" + }, + { + "publisher_id": 68, + "dataset": "DBEQ.MAX", + "venue": "FINN", + "description": "DBEQ Max - FINRA/NYSE TRF" + }, + { + "publisher_id": 69, + "dataset": "DBEQ.MAX", + "venue": "FINY", + "description": "DBEQ Max - FINRA/Nasdaq TRF Carteret" + }, + { + "publisher_id": 70, + "dataset": "DBEQ.MAX", + "venue": "FINC", + "description": "DBEQ Max - FINRA/Nasdaq TRF Chicago" + }, + { + "publisher_id": 71, + "dataset": "DBEQ.MAX", + "venue": "BATS", + "description": "DBEQ Max - CBOE BZX" + }, + { + "publisher_id": 72, + "dataset": "DBEQ.MAX", + "venue": "BATY", + "description": "DBEQ Max - CBOE BYX" + }, + { + "publisher_id": 73, + "dataset": "DBEQ.MAX", + "venue": "EDGA", + "description": "DBEQ Max - CBOE EDGA" + }, + { + "publisher_id": 74, + "dataset": "DBEQ.MAX", + "venue": "EDGX", + "description": "DBEQ Max - CBOE EDGX" + }, + { + "publisher_id": 75, + "dataset": "DBEQ.MAX", + "venue": "XBOS", + "description": "DBEQ Max - Nasdaq BX" + }, + { + "publisher_id": 76, + "dataset": "DBEQ.MAX", + "venue": "XPSX", + "description": "DBEQ Max - Nasdaq PSX" + }, + { + "publisher_id": 77, + "dataset": "DBEQ.MAX", + "venue": "MEMX", + "description": "DBEQ Max - MEMX" + }, + { + "publisher_id": 78, + "dataset": "DBEQ.MAX", + "venue": "XASE", + "description": "DBEQ Max - NYSE American" + }, + { + "publisher_id": 79, + "dataset": "DBEQ.MAX", + "venue": "ARCX", + "description": "DBEQ Max - NYSE Arca" + }, + { + "publisher_id": 80, + "dataset": "DBEQ.MAX", + "venue": "LTSE", + "description": "DBEQ Max - Long-Term Stock Exchange" } ] diff --git a/nautilus_trader/adapters/databento/publishers.json b/nautilus_trader/adapters/databento/publishers.json index f9f45ff0c837..cbd2492e6c9b 100644 --- a/nautilus_trader/adapters/databento/publishers.json +++ b/nautilus_trader/adapters/databento/publishers.json @@ -364,5 +364,119 @@ "dataset": "OPRA.PILLAR", "venue": "SPHR", "description": "OPRA - MIAX Sapphire" + }, + { + "publisher_id": 62, + "dataset": "DBEQ.MAX", + "venue": "XCHI", + "description": "DBEQ Max - NYSE Chicago" + }, + { + "publisher_id": 63, + "dataset": "DBEQ.MAX", + "venue": "XCIS", + "description": "DBEQ Max - NYSE National" + }, + { + "publisher_id": 64, + "dataset": "DBEQ.MAX", + "venue": "IEXG", + "description": "DBEQ Max - IEX" + }, + { + "publisher_id": 65, + "dataset": "DBEQ.MAX", + "venue": "EPRL", + "description": "DBEQ Max - MIAX Pearl" + }, + { + "publisher_id": 66, + "dataset": "DBEQ.MAX", + "venue": "XNAS", + "description": "DBEQ Max - Nasdaq" + }, + { + "publisher_id": 67, + "dataset": "DBEQ.MAX", + "venue": "XNYS", + "description": "DBEQ Max - NYSE" + }, + { + "publisher_id": 68, + "dataset": "DBEQ.MAX", + "venue": "FINN", + "description": "DBEQ Max - FINRA/NYSE TRF" + }, + { + "publisher_id": 69, + "dataset": "DBEQ.MAX", + "venue": "FINY", + "description": "DBEQ Max - FINRA/Nasdaq TRF Carteret" + }, + { + "publisher_id": 70, + "dataset": "DBEQ.MAX", + "venue": "FINC", + "description": "DBEQ Max - FINRA/Nasdaq TRF Chicago" + }, + { + "publisher_id": 71, + "dataset": "DBEQ.MAX", + "venue": "BATS", + "description": "DBEQ Max - CBOE BZX" + }, + { + "publisher_id": 72, + "dataset": "DBEQ.MAX", + "venue": "BATY", + "description": "DBEQ Max - CBOE BYX" + }, + { + "publisher_id": 73, + "dataset": "DBEQ.MAX", + "venue": "EDGA", + "description": "DBEQ Max - CBOE EDGA" + }, + { + "publisher_id": 74, + "dataset": "DBEQ.MAX", + "venue": "EDGX", + "description": "DBEQ Max - CBOE EDGX" + }, + { + "publisher_id": 75, + "dataset": "DBEQ.MAX", + "venue": "XBOS", + "description": "DBEQ Max - Nasdaq BX" + }, + { + "publisher_id": 76, + "dataset": "DBEQ.MAX", + "venue": "XPSX", + "description": "DBEQ Max - Nasdaq PSX" + }, + { + "publisher_id": 77, + "dataset": "DBEQ.MAX", + "venue": "MEMX", + "description": "DBEQ Max - MEMX" + }, + { + "publisher_id": 78, + "dataset": "DBEQ.MAX", + "venue": "XASE", + "description": "DBEQ Max - NYSE American" + }, + { + "publisher_id": 79, + "dataset": "DBEQ.MAX", + "venue": "ARCX", + "description": "DBEQ Max - NYSE Arca" + }, + { + "publisher_id": 80, + "dataset": "DBEQ.MAX", + "venue": "LTSE", + "description": "DBEQ Max - Long-Term Stock Exchange" } ] From 5e8ba20ad56cadcee49d9547b934dfecbded2318 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 08:18:32 +1100 Subject: [PATCH 66/71] Add Databento continuous symbology support --- RELEASES.md | 3 +- .../live/databento/databento_subscriber.py | 6 +- .../adapters/src/databento/common.rs | 93 +++++++++++++++++++ .../src/databento/python/historical.rs | 39 ++++++-- .../adapters/src/databento/python/live.rs | 8 +- nautilus_trader/adapters/databento/data.py | 34 +++---- .../adapters/databento/providers.py | 6 +- nautilus_trader/core/nautilus_pyo3.pyi | 14 +-- .../adapters/databento/test_loaders.py | 2 +- 9 files changed, 160 insertions(+), 45 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 3c2867884ce2..bef918469873 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,8 +3,9 @@ Released on TBD (UTC). ### Enhancements +- Added Databento adapter continuous symbology support (will infer from symbols) - Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection -- Added CSV tick and bar data loaders params, thanks @rterbush +- Added CSV tick and bar data loader params, thanks @rterbush - Implemented `LogGuard` to ensure global logger is flushed on termination, thanks @ayush-sb and @twitu - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py index 4f69ee4e0e32..e93617570cc7 100644 --- a/examples/live/databento/databento_subscriber.py +++ b/examples/live/databento/databento_subscriber.py @@ -44,8 +44,8 @@ # For correct subscription operation, you must specify all instruments to be immediately # subscribed for as part of the data client configuration instrument_ids = [ - InstrumentId.from_str("ESM4.GLBX"), - # InstrumentId.from_str("ESU4.GLBX"), + # InstrumentId.from_str("ESM4.GLBX"), + InstrumentId.from_str("ES.c.0.GLBX"), # InstrumentId.from_str("AAPL.XNAS"), ] @@ -153,7 +153,7 @@ def on_start(self) -> None: # ) self.subscribe_quote_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) - # self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) + self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) # self.request_quote_ticks(instrument_id) # self.request_trade_ticks(instrument_id) diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs index ce5e13097731..be9bab0aa09a 100644 --- a/nautilus_core/adapters/src/databento/common.rs +++ b/nautilus_core/adapters/src/databento/common.rs @@ -26,3 +26,96 @@ pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result String { + if symbol.ends_with(".FUT") || symbol.ends_with(".OPT") { + return "parent".to_string(); + } + + let parts: Vec<&str> = symbol.split('.').collect(); + if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) { + return "continuous".to_string(); + } + + "raw_symbol".to_string() +} + +pub fn check_consistent_symbology(symbols: &[&str]) -> anyhow::Result<()> { + if symbols.is_empty() { + return Err(anyhow::anyhow!("Symbols was empty")); + }; + + // SAFETY: We checked len so know there must be at least one symbol + let first_symbol = symbols.first().unwrap(); + let first_stype = infer_symbology_type(first_symbol); + + for symbol in symbols { + let next_stype = infer_symbology_type(symbol); + if next_stype != first_stype { + return Err(anyhow::anyhow!( + "Inconsistent symbology types: '{}' for {} vs '{}' for {}", + first_stype, + first_symbol, + next_stype, + symbol + )); + } + } + + Ok(()) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::*; + + use super::*; + + #[rstest] + #[case("AAPL", "raw_symbol")] + #[case("ESM4", "raw_symbol")] + #[case("BRN FMM0024!", "raw_symbol")] + #[case("BRN 99 5617289", "raw_symbol")] + #[case("SPY 240319P00511000", "raw_symbol")] + #[case("ES.FUT", "parent")] + #[case("ES.OPT", "parent")] + #[case("BRN.FUT", "parent")] + #[case("SPX.OPT", "parent")] + #[case("ES.c.0", "continuous")] + #[case("SPX.n.0", "continuous")] + fn test_infer_symbology_type(#[case] symbol: String, #[case] expected: String) { + let result = infer_symbology_type(&symbol); + assert_eq!(result, expected); + } + + #[rstest] + fn test_check_consistent_symbology_when_empty_symbols() { + let symbols: Vec<&str> = vec![]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!(result.err().unwrap().to_string(), "Symbols was empty"); + } + + #[rstest] + fn test_check_consistent_symbology_when_inconsistent() { + let symbols = vec!["ESM4", "ES.OPT"]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Inconsistent symbology types: 'raw_symbol' for ESM4 vs 'parent' for ES.OPT" + ); + } + + #[rstest] + #[case(vec!["AAPL,MSFT"])] + #[case(vec!["ES.OPT,ES.FUT"])] + #[case(vec!["ES.c.0,ES.c.1"])] + fn test_check_consistent_symbology_when_consistent(#[case] symbols: Vec<&str>) { + let result = check_consistent_symbology(&symbols); + assert!(result.is_ok()); + } +} diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index 1d92bd51d5e4..758bfdd0949e 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -13,9 +13,12 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{fs, num::NonZeroU64, sync::Arc}; +use std::{fs, num::NonZeroU64, str::FromStr, sync::Arc}; -use databento::{dbn, historical::timeseries::GetRangeParams}; +use databento::{ + dbn::{self, SType}, + historical::timeseries::GetRangeParams, +}; use indexmap::IndexMap; use nautilus_core::{ python::to_pyvalue_err, @@ -36,7 +39,7 @@ use tokio::sync::Mutex; use super::loader::convert_instrument_to_pyobject; use crate::databento::{ - common::get_date_time_range, + common::{check_consistent_symbology, get_date_time_range, infer_symbology_type}, decode::{ decode_imbalance_msg, decode_instrument_def_msg, decode_record, decode_statistics_msg, raw_ptr_to_ustr, @@ -110,19 +113,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Definition) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -175,19 +181,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Mbp1) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -239,19 +248,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Trades) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -304,7 +316,7 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, aggregation: BarAggregation, start: UnixNanos, end: Option, @@ -312,6 +324,8 @@ impl DatabentoHistoricalClient { ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let schema = match aggregation { BarAggregation::Second => dbn::Schema::Ohlcv1S, BarAggregation::Minute => dbn::Schema::Ohlcv1M, @@ -325,6 +339,7 @@ impl DatabentoHistoricalClient { .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(schema) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -377,19 +392,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Imbalance) .limit(limit.and_then(NonZeroU64::new)) .build(); @@ -431,19 +449,22 @@ impl DatabentoHistoricalClient { &self, py: Python<'py>, dataset: String, - symbols: String, + symbols: Vec<&str>, start: UnixNanos, end: Option, limit: Option, ) -> PyResult<&'py PyAny> { let client = self.inner.clone(); + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let end = end.unwrap_or(self.clock.get_time_ns()); let time_range = get_date_time_range(start, end).map_err(to_pyvalue_err)?; let params = GetRangeParams::builder() .dataset(dataset) .date_time_range(time_range) .symbols(symbols) + .stype_in(SType::from_str(&stype_in).map_err(to_pyvalue_err)?) .schema(dbn::Schema::Statistics) .limit(limit.and_then(NonZeroU64::new)) .build(); diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index ef0a99135bfc..9185bdfee62a 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -29,6 +29,7 @@ use tracing::{debug, error, trace}; use super::loader::convert_instrument_to_pyobject; use crate::databento::{ + common::{check_consistent_symbology, infer_symbology_type}, live::{DatabentoFeedHandler, LiveCommand, LiveMessage}, types::DatabentoPublisher, }; @@ -150,12 +151,11 @@ impl DatabentoLiveClient { fn py_subscribe( &mut self, schema: String, - symbols: String, - stype_in: Option, + symbols: Vec<&str>, start: Option, ) -> PyResult<()> { - let stype_in = stype_in.unwrap_or("raw_symbol".to_string()); - + let stype_in = infer_symbology_type(symbols.first().unwrap()); + check_consistent_symbology(symbols.as_slice()).map_err(to_pyvalue_err)?; let mut sub = Subscription::builder() .symbols(symbols) .schema(dbn::Schema::from_str(&schema).map_err(to_pyvalue_err)?) diff --git a/nautilus_trader/adapters/databento/data.py b/nautilus_trader/adapters/databento/data.py index e00bfbebc478..4b8285160d89 100644 --- a/nautilus_trader/adapters/databento/data.py +++ b/nautilus_trader/adapters/databento/data.py @@ -387,7 +387,7 @@ async def _subscribe_imbalance(self, data_type: DataType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.IMBALANCE.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -401,7 +401,7 @@ async def _subscribe_statistics(self, data_type: DataType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.STATISTICS.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -417,7 +417,7 @@ async def _subscribe_instrument(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -432,7 +432,7 @@ async def _subscribe_parent_symbols( live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted(parent_symbols)), + symbols=sorted(parent_symbols), stype_in="parent", ) await self._check_live_client_started(dataset, live_client) @@ -448,7 +448,7 @@ async def _subscribe_instrument_ids( live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=[i.symbol.value for i in instrument_ids], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -533,7 +533,7 @@ async def _subscribe_order_book_deltas_batch( live_client.subscribe( schema=DatabentoSchema.MBO.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=[i.symbol.value for i in instrument_ids], start=0, # Replay from start of weekly session ) @@ -578,7 +578,7 @@ async def _subscribe_order_book_snapshots( live_client = self._get_live_client(dataset) live_client.subscribe( schema=schema, - symbols=",".join(sorted([instrument_id.symbol.value])), + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -592,7 +592,7 @@ async def _subscribe_quote_ticks(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.MBP_1.value, - symbols=",".join(sorted([instrument_id.symbol.value])), + symbols=[instrument_id.symbol.value], ) # Add trade tick subscriptions for instrument (MBP-1 data includes trades) @@ -613,7 +613,7 @@ async def _subscribe_trade_ticks(self, instrument_id: InstrumentId) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=DatabentoSchema.TRADES.value, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -632,7 +632,7 @@ async def _subscribe_bars(self, bar_type: BarType) -> None: live_client = self._get_live_client(dataset) live_client.subscribe( schema=schema.value, - symbols=bar_type.instrument_id.symbol.value, + symbols=[bar_type.instrument_id.symbol.value], ) await self._check_live_client_started(dataset, live_client) except asyncio.CancelledError: @@ -713,7 +713,7 @@ async def _request_imbalance(self, data_type: DataType, correlation_id: UUID4) - pyo3_imbalances = await self._http_client.get_range_imbalance( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -743,7 +743,7 @@ async def _request_statistics(self, data_type: DataType, correlation_id: UUID4) pyo3_statistics = await self._http_client.get_range_statistics( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -775,7 +775,7 @@ async def _request_instrument( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -808,7 +808,7 @@ async def _request_instruments( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[ALL_SYMBOLS], start=start.value, end=end.value, ) @@ -848,7 +848,7 @@ async def _request_quote_ticks( pyo3_quotes = await self._http_client.get_range_quotes( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=start.value, end=end.value, ) @@ -888,7 +888,7 @@ async def _request_trade_ticks( pyo3_trades = await self._http_client.get_range_trades( dataset=dataset, - symbols=instrument_id.symbol.value, + symbols=[instrument_id.symbol.value], start=(start or available_end - pd.Timedelta(days=1)).value, end=(end or available_end).value, ) @@ -928,7 +928,7 @@ async def _request_bars( pyo3_bars = await self._http_client.get_range_bars( dataset=dataset, - symbols=bar_type.instrument_id.symbol.value, + symbols=[bar_type.instrument_id.symbol.value], aggregation=nautilus_pyo3.BarAggregation( bar_aggregation_to_str(bar_type.spec.aggregation), ), diff --git a/nautilus_trader/adapters/databento/providers.py b/nautilus_trader/adapters/databento/providers.py index 583939692d29..caff1c893ad5 100644 --- a/nautilus_trader/adapters/databento/providers.py +++ b/nautilus_trader/adapters/databento/providers.py @@ -137,7 +137,7 @@ def receive_instruments(pyo3_instrument: Any) -> None: live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, - symbols=",".join(sorted([i.symbol.value for i in instrument_ids])), + symbols=sorted([i.symbol.value for i in instrument_ids]), start=0, # From start of current week (latest definitions) ) @@ -146,7 +146,7 @@ def receive_instruments(pyo3_instrument: Any) -> None: live_client.subscribe( schema=DatabentoSchema.DEFINITION.value, stype_in="parent", - symbols=",".join(parent_symbols), + symbols=parent_symbols, start=0, # From start of current week (latest definitions) ) @@ -236,7 +236,7 @@ async def get_range( pyo3_instruments = await self._http_client.get_range_instruments( dataset=dataset, - symbols=ALL_SYMBOLS, + symbols=[ALL_SYMBOLS], start=pd.Timestamp(start, tz=pytz.utc).value, end=pd.Timestamp(end, tz=pytz.utc).value if end is not None else None, ) diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 7ab9b8b908b0..43417c96fa9a 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -2532,7 +2532,7 @@ class DatabentoHistoricalClient: async def get_range_instruments( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2540,7 +2540,7 @@ class DatabentoHistoricalClient: async def get_range_quotes( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2548,7 +2548,7 @@ class DatabentoHistoricalClient: async def get_range_trades( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2556,7 +2556,7 @@ class DatabentoHistoricalClient: async def get_range_bars( self, dataset: str, - symbols: str, + symbols: list[str], aggregation: BarAggregation, start: int, end: int | None = None, @@ -2565,7 +2565,7 @@ class DatabentoHistoricalClient: async def get_range_imbalance( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2573,7 +2573,7 @@ class DatabentoHistoricalClient: async def get_range_statistics( self, dataset: str, - symbols: str, + symbols: list[str], start: int, end: int | None = None, limit: int | None = None, @@ -2597,7 +2597,7 @@ class DatabentoLiveClient: def subscribe( self, schema: str, - symbols: str, + symbols: list[str], stype_in: str | None = None, start: int | None = None, ) -> dict[str, str]: ... diff --git a/tests/integration_tests/adapters/databento/test_loaders.py b/tests/integration_tests/adapters/databento/test_loaders.py index 4f8d0de90bbb..f1918a7b764d 100644 --- a/tests/integration_tests/adapters/databento/test_loaders.py +++ b/tests/integration_tests/adapters/databento/test_loaders.py @@ -52,7 +52,7 @@ def test_get_publishers() -> None: result = loader.get_publishers() # Assert - assert len(result) == 61 # From built-in map + assert len(result) == 80 # From built-in map def test_loader_definition_glbx_futures() -> None: From abe797ad82f007104c3e5df07bd2e4d7c4bc0686 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 11:09:10 +1100 Subject: [PATCH 67/71] Upgrade Rust --- README.md | 8 ++++---- nautilus_core/Cargo.lock | 4 ++-- nautilus_core/Cargo.toml | 2 +- nautilus_core/adapters/src/databento/common.rs | 1 + nautilus_core/rust-toolchain.toml | 2 +- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 66587f577ec2..48c1b629811e 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.76.0+ | 3.10+ | -| `macOS (x86_64)` | 1.76.0+ | 3.10+ | -| `macOS (arm64)` | 1.76.0+ | 3.10+ | -| `Windows (x86_64)` | 1.76.0+ | 3.10+ | +| `Linux (x86_64)` | 1.77.0+ | 3.10+ | +| `macOS (x86_64)` | 1.77.0+ | 3.10+ | +| `macOS (arm64)` | 1.77.0+ | 3.10+ | +| `Windows (x86_64)` | 1.77.0+ | 3.10+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 255b2452ae5e..5fddd4b53453 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "95d8e92cac0961e91dbd517496b00f7e9b92363dbe6d42c3198268323798860c" dependencies = [ "addr2line", "cc", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index c919535e9ab4..b92f9b6eaa41 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -17,7 +17,7 @@ members = [ ] [workspace.package] -rust-version = "1.76.0" +rust-version = "1.77.0" version = "0.20.0" edition = "2021" authors = ["Nautech Systems "] diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs index be9bab0aa09a..0c70dc5d56e0 100644 --- a/nautilus_core/adapters/src/databento/common.rs +++ b/nautilus_core/adapters/src/databento/common.rs @@ -27,6 +27,7 @@ pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result String { if symbol.ends_with(".FUT") || symbol.ends_with(".OPT") { return "parent".to_string(); diff --git a/nautilus_core/rust-toolchain.toml b/nautilus_core/rust-toolchain.toml index 8299b1d792e7..2a1ad66785f1 100644 --- a/nautilus_core/rust-toolchain.toml +++ b/nautilus_core/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.76.0" +version = "1.77.0" channel = "stable" diff --git a/poetry.lock b/poetry.lock index 471d16086719..bf985ce073f4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1760,13 +1760,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-mock" -version = "3.13.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.13.0.tar.gz", hash = "sha256:58c73e7eae1fc05aebe7bd4b3b59eb5beb5c0b644c7e2dd3cc652a749f0056e3"}, - {file = "pytest_mock-3.13.0-py3-none-any.whl", hash = "sha256:158462fe2124763df267f20c81f098bdccf8928e2649e5e1093955ad1e515f80"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] @@ -2611,4 +2611,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "8c0e6f05e530ec3be9f6fde0ff69a9ca769a5ca60e0de65d79d2b3b385af5053" +content-hash = "61899ddfdeb6e2422bd7a06565219da0a6805023c1a6fb3767a022a8eed95b01" diff --git a/pyproject.toml b/pyproject.toml index c1ea1a13557a..6b656d246b93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type pytest-benchmark = "^4.0.0" pytest-cov = "^4.1.0" -pytest-mock = "^3.13.0" +pytest-mock = "^3.14.0" pytest-xdist = { version = "^3.5.0", extras = ["psutil"] } [tool.poetry.group.docs] From 7e6acbe39f89fcdf6a363de08dd696b26ef6d52b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 18:04:05 +1100 Subject: [PATCH 68/71] Add Databento instrument_id symbology support --- .../adapters/src/databento/common.rs | 94 ---------------- .../src/databento/python/historical.rs | 4 +- .../adapters/src/databento/python/live.rs | 2 +- .../adapters/src/databento/symbology.rs | 100 ++++++++++++++++++ 4 files changed, 103 insertions(+), 97 deletions(-) diff --git a/nautilus_core/adapters/src/databento/common.rs b/nautilus_core/adapters/src/databento/common.rs index 0c70dc5d56e0..ce5e13097731 100644 --- a/nautilus_core/adapters/src/databento/common.rs +++ b/nautilus_core/adapters/src/databento/common.rs @@ -26,97 +26,3 @@ pub fn get_date_time_range(start: UnixNanos, end: UnixNanos) -> anyhow::Result String { - if symbol.ends_with(".FUT") || symbol.ends_with(".OPT") { - return "parent".to_string(); - } - - let parts: Vec<&str> = symbol.split('.').collect(); - if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) { - return "continuous".to_string(); - } - - "raw_symbol".to_string() -} - -pub fn check_consistent_symbology(symbols: &[&str]) -> anyhow::Result<()> { - if symbols.is_empty() { - return Err(anyhow::anyhow!("Symbols was empty")); - }; - - // SAFETY: We checked len so know there must be at least one symbol - let first_symbol = symbols.first().unwrap(); - let first_stype = infer_symbology_type(first_symbol); - - for symbol in symbols { - let next_stype = infer_symbology_type(symbol); - if next_stype != first_stype { - return Err(anyhow::anyhow!( - "Inconsistent symbology types: '{}' for {} vs '{}' for {}", - first_stype, - first_symbol, - next_stype, - symbol - )); - } - } - - Ok(()) -} - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// -#[cfg(test)] -mod tests { - use rstest::*; - - use super::*; - - #[rstest] - #[case("AAPL", "raw_symbol")] - #[case("ESM4", "raw_symbol")] - #[case("BRN FMM0024!", "raw_symbol")] - #[case("BRN 99 5617289", "raw_symbol")] - #[case("SPY 240319P00511000", "raw_symbol")] - #[case("ES.FUT", "parent")] - #[case("ES.OPT", "parent")] - #[case("BRN.FUT", "parent")] - #[case("SPX.OPT", "parent")] - #[case("ES.c.0", "continuous")] - #[case("SPX.n.0", "continuous")] - fn test_infer_symbology_type(#[case] symbol: String, #[case] expected: String) { - let result = infer_symbology_type(&symbol); - assert_eq!(result, expected); - } - - #[rstest] - fn test_check_consistent_symbology_when_empty_symbols() { - let symbols: Vec<&str> = vec![]; - let result = check_consistent_symbology(&symbols); - assert!(result.is_err()); - assert_eq!(result.err().unwrap().to_string(), "Symbols was empty"); - } - - #[rstest] - fn test_check_consistent_symbology_when_inconsistent() { - let symbols = vec!["ESM4", "ES.OPT"]; - let result = check_consistent_symbology(&symbols); - assert!(result.is_err()); - assert_eq!( - result.err().unwrap().to_string(), - "Inconsistent symbology types: 'raw_symbol' for ESM4 vs 'parent' for ES.OPT" - ); - } - - #[rstest] - #[case(vec!["AAPL,MSFT"])] - #[case(vec!["ES.OPT,ES.FUT"])] - #[case(vec!["ES.c.0,ES.c.1"])] - fn test_check_consistent_symbology_when_consistent(#[case] symbols: Vec<&str>) { - let result = check_consistent_symbology(&symbols); - assert!(result.is_ok()); - } -} diff --git a/nautilus_core/adapters/src/databento/python/historical.rs b/nautilus_core/adapters/src/databento/python/historical.rs index 758bfdd0949e..f8210823ea87 100644 --- a/nautilus_core/adapters/src/databento/python/historical.rs +++ b/nautilus_core/adapters/src/databento/python/historical.rs @@ -39,12 +39,12 @@ use tokio::sync::Mutex; use super::loader::convert_instrument_to_pyobject; use crate::databento::{ - common::{check_consistent_symbology, get_date_time_range, infer_symbology_type}, + common::get_date_time_range, decode::{ decode_imbalance_msg, decode_instrument_def_msg, decode_record, decode_statistics_msg, raw_ptr_to_ustr, }, - symbology::decode_nautilus_instrument_id, + symbology::{check_consistent_symbology, decode_nautilus_instrument_id, infer_symbology_type}, types::{DatabentoImbalance, DatabentoPublisher, DatabentoStatistics, PublisherId}, }; diff --git a/nautilus_core/adapters/src/databento/python/live.rs b/nautilus_core/adapters/src/databento/python/live.rs index 9185bdfee62a..44dffbd62bb7 100644 --- a/nautilus_core/adapters/src/databento/python/live.rs +++ b/nautilus_core/adapters/src/databento/python/live.rs @@ -29,8 +29,8 @@ use tracing::{debug, error, trace}; use super::loader::convert_instrument_to_pyobject; use crate::databento::{ - common::{check_consistent_symbology, infer_symbology_type}, live::{DatabentoFeedHandler, LiveCommand, LiveMessage}, + symbology::{check_consistent_symbology, infer_symbology_type}, types::DatabentoPublisher, }; diff --git a/nautilus_core/adapters/src/databento/symbology.rs b/nautilus_core/adapters/src/databento/symbology.rs index f424649cf589..8bd99bb691b0 100644 --- a/nautilus_core/adapters/src/databento/symbology.rs +++ b/nautilus_core/adapters/src/databento/symbology.rs @@ -72,3 +72,103 @@ pub fn get_nautilus_instrument_id_for_record( Ok(InstrumentId::new(symbol, venue)) } + +#[must_use] +pub fn infer_symbology_type(symbol: &str) -> String { + if symbol.ends_with(".FUT") || symbol.ends_with(".OPT") { + return "parent".to_string(); + } + + let parts: Vec<&str> = symbol.split('.').collect(); + if parts.len() == 3 && parts[2].chars().all(|c| c.is_ascii_digit()) { + return "continuous".to_string(); + } + + if symbol.chars().all(|c| c.is_ascii_digit()) { + return "instrument_id".to_string(); + } + + "raw_symbol".to_string() +} + +pub fn check_consistent_symbology(symbols: &[&str]) -> anyhow::Result<()> { + if symbols.is_empty() { + return Err(anyhow::anyhow!("Symbols was empty")); + }; + + // SAFETY: We checked len so know there must be at least one symbol + let first_symbol = symbols.first().unwrap(); + let first_stype = infer_symbology_type(first_symbol); + + for symbol in symbols { + let next_stype = infer_symbology_type(symbol); + if next_stype != first_stype { + return Err(anyhow::anyhow!( + "Inconsistent symbology types: '{}' for {} vs '{}' for {}", + first_stype, + first_symbol, + next_stype, + symbol + )); + } + } + + Ok(()) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::*; + + use super::*; + + #[rstest] + #[case("1", "instrument_id")] + #[case("123456789", "instrument_id")] + #[case("AAPL", "raw_symbol")] + #[case("ESM4", "raw_symbol")] + #[case("BRN FMM0024!", "raw_symbol")] + #[case("BRN 99 5617289", "raw_symbol")] + #[case("SPY 240319P00511000", "raw_symbol")] + #[case("ES.FUT", "parent")] + #[case("ES.OPT", "parent")] + #[case("BRN.FUT", "parent")] + #[case("SPX.OPT", "parent")] + #[case("ES.c.0", "continuous")] + #[case("SPX.n.0", "continuous")] + fn test_infer_symbology_type(#[case] symbol: String, #[case] expected: String) { + let result = infer_symbology_type(&symbol); + assert_eq!(result, expected); + } + + #[rstest] + fn test_check_consistent_symbology_when_empty_symbols() { + let symbols: Vec<&str> = vec![]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!(result.err().unwrap().to_string(), "Symbols was empty"); + } + + #[rstest] + fn test_check_consistent_symbology_when_inconsistent() { + let symbols = vec!["ESM4", "ES.OPT"]; + let result = check_consistent_symbology(&symbols); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap().to_string(), + "Inconsistent symbology types: 'raw_symbol' for ESM4 vs 'parent' for ES.OPT" + ); + } + + #[rstest] + #[case(vec!["AAPL,MSFT"])] + #[case(vec!["ES.OPT,ES.FUT"])] + #[case(vec!["ES.c.0,ES.c.1"])] + fn test_check_consistent_symbology_when_consistent(#[case] symbols: Vec<&str>) { + let result = check_consistent_symbology(&symbols); + assert!(result.is_ok()); + } +} From 2436c95f05a0003817d0807c8c489519ea26cc5b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 19:14:52 +1100 Subject: [PATCH 69/71] Update integrations docs --- docs/integrations/index.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 1861de65b06f..39028fb41ed7 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -34,6 +34,14 @@ running strategies which are able to access larger capital allocations. | [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | +- `ID:` The default client ID for the integrations adapter clients +- `Type:` The type of integration (often the venue type) + +### Status +- `building` - Under construction and likely not in a usable state +- `beta` - Completed to a minimally working state and in a 'beta' testing phase +- `stable` - Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain) + ## Implementation goals The primary goal of NautilusTrader is to provide a unified trading system for From 6d1eb12c8b7074a1896f8bc64164836f1ace295d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 19:18:10 +1100 Subject: [PATCH 70/71] Update README and release notes --- README.md | 14 ++++++++++++-- RELEASES.md | 14 +++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 48c1b629811e..85de4c767bbc 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,8 @@ This project makes the [Soundness Pledge](https://raphlinus.github.io/rust/2020/ ## Integrations -NautilusTrader is designed in a modular way to work with 'adapters' which provide -connectivity to data providers and/or trading venues - converting their raw API +NautilusTrader is designed in a modular way to work with _adapters_ which provide +connectivity to trading venues and/or data providers - converting their raw API into a unified interface. The following integrations are currently supported: | Name | ID | Type | Status | Docs | @@ -149,7 +149,17 @@ into a unified interface. The following integrations are currently supported: | [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/databento.html) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | +- `ID:` The default client ID for the integrations adapter clients +- `Type:` The type of integration (often the venue type) + +### Status +- `building` - Under construction and likely not in a usable state +- `beta` - Completed to a minimally working state and in a 'beta' testing phase +- `stable` - Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain) + +```{note} Refer to the [Integrations](https://docs.nautilustrader.io/integrations/index.html) documentation for further details. +``` ## Installation diff --git a/RELEASES.md b/RELEASES.md index bef918469873..6d5588599992 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,27 +1,27 @@ # NautilusTrader 1.190.0 Beta -Released on TBD (UTC). +Released on 22nd March 2024 (UTC). ### Enhancements -- Added Databento adapter continuous symbology support (will infer from symbols) +- Added Databento adapter `continuous`, `parent` and `instrument_id` symbology support (will infer from symbols) - Added `DatabaseConfig.timeout` config option for timeout seconds to wait for a new connection - Added CSV tick and bar data loader params, thanks @rterbush - Implemented `LogGuard` to ensure global logger is flushed on termination, thanks @ayush-sb and @twitu +- Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton - Improved Binance execution client ping listen key error handling and logging - Improved Redis cache adapter and message bus error handling and logging -- Improved Interactive Brokers client connectivity resilience and component lifecycle, thanks @benjaminsingleton - Improved Redis port parsing (`DatabaseConfig.port` can now be either a string or integer) -- Redact Redis passwords in strings and logs - Refactored `InteractiveBrokersEWrapper`, thanks @rsmb7z -- Upgraded `redis` crate to 0.25.1 which bumps up TLS dependencies +- Redact Redis passwords in strings and logs +- Upgraded `redis` crate to 0.25.2 which bumps up TLS dependencies, and turned on `tls-rustls-webpki-roots` feature flag ### Breaking Changes None ### Fixes - Fixed JSON format for log file output (was missing `timestamp` and `trader\_id`) -- Fixed `DatabaseConfig` port JSON parsing for Redis (was always falling back to the default 6379) -- Fixed `ChandeMomentumOscillator` indicator divide by zero error +- Fixed `DatabaseConfig` port JSON parsing for Redis (was always defaulting to 6379) +- Fixed `ChandeMomentumOscillator` indicator divide by zero error (both Rust and Cython versions) --- From 6c315af8545ce958a3789cf5029815430e2ad214 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Mar 2024 20:11:03 +1100 Subject: [PATCH 71/71] Update README and integration docs --- README.md | 4 +--- docs/integrations/index.md | 11 ++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 85de4c767bbc..73a48897ff9d 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ This project makes the [Soundness Pledge](https://raphlinus.github.io/rust/2020/ ## Integrations NautilusTrader is designed in a modular way to work with _adapters_ which provide -connectivity to trading venues and/or data providers - converting their raw API +connectivity to trading venues and data providers - converting their raw API into a unified interface. The following integrations are currently supported: | Name | ID | Type | Status | Docs | @@ -157,9 +157,7 @@ into a unified interface. The following integrations are currently supported: - `beta` - Completed to a minimally working state and in a 'beta' testing phase - `stable` - Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain) -```{note} Refer to the [Integrations](https://docs.nautilustrader.io/integrations/index.html) documentation for further details. -``` ## Installation diff --git a/docs/integrations/index.md b/docs/integrations/index.md index 39028fb41ed7..123fdc6c13b7 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -11,19 +11,12 @@ binance.md databento.md ib.md - ``` -NautilusTrader is designed in a modular way to work with 'adapters' which provide -connectivity to data providers and/or trading venues - converting their raw API +NautilusTrader is designed in a modular way to work with *adapters* which provide +connectivity to trading venues and data providers - converting their raw API into a unified interface. The following integrations are currently supported: -```{warning} -The initial integrations for the project are currently under heavy construction. -It's advised to conduct some of your own testing with small amounts of capital before -running strategies which are able to access larger capital allocations. -``` - | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :-------------------- | :---------------------- | :------------------------------------------------------ | :------------------------------------------------------------------ | | [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) |