From a77c491d89313bd08194aa328853962c33803c2b Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 29 Oct 2023 15:07:50 +0100 Subject: [PATCH] CurrencyPair in rust with pyo3 --- .../model/src/instruments/currency_pair.rs | 87 ++++++++--- .../src/python/instruments/currency_pair.rs | 136 ++++++++++++++++++ .../model/src/python/instruments/mod.rs | 1 + nautilus_trader/core/nautilus_pyo3.pyi | 2 +- nautilus_trader/test_kit/rust/instruments.py | 23 +++ .../instruments/test_crypto_future_pyo3.py | 2 +- .../instruments/test_crypto_perpetual_pyo3.py | 2 +- .../instruments/test_currency_pair_pyo3.py | 55 +++++++ 8 files changed, 289 insertions(+), 19 deletions(-) create mode 100644 nautilus_core/model/src/python/instruments/currency_pair.rs create mode 100644 tests/unit_tests/model/instruments/test_currency_pair_pyo3.py diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 00f93f0346ee..ba130bf842ab 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -18,6 +18,7 @@ use std::hash::{Hash, Hasher}; use pyo3::prelude::*; +use anyhow::Result; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -43,19 +44,18 @@ pub struct CurrencyPair { pub size_precision: u8, pub price_increment: Price, pub size_increment: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, + pub maker_fee: Decimal, + pub taker_fee: Decimal, pub lot_size: Option, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, pub min_price: Option, - pub margin_init: Decimal, - pub margin_maint: Decimal, - pub maker_fee: Decimal, - pub taker_fee: Decimal, } impl CurrencyPair { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, @@ -66,17 +66,17 @@ impl CurrencyPair { size_precision: u8, price_increment: Price, size_increment: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, lot_size: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, - margin_init: Decimal, - margin_maint: Decimal, - maker_fee: Decimal, - taker_fee: Decimal, - ) -> Self { - Self { + ) -> Result { + Ok(Self { id, raw_symbol, quote_currency, @@ -85,16 +85,16 @@ impl CurrencyPair { size_precision, price_increment, size_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, lot_size, max_quantity, min_quantity, max_price, min_price, - margin_init, - margin_maint, - maker_fee, - taker_fee, - } + }) } } @@ -202,3 +202,58 @@ impl Instrument for CurrencyPair { self.taker_fee } } + + +//////////////////////////////////////////////////////////////////////////////// +// Stubs +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +pub mod stubs{ + use std::str::FromStr; + use rstest::fixture; + use rust_decimal::Decimal; + use crate::identifiers::instrument_id::InstrumentId; + use crate::identifiers::symbol::Symbol; + use crate::instruments::currency_pair::CurrencyPair; + use crate::types::currency::Currency; + use crate::types::price::Price; + use crate::types::quantity::Quantity; + + #[fixture] + pub fn currency_pair_btcusdt()-> CurrencyPair{ + CurrencyPair::new( + InstrumentId::from("BTCUSDT.BINANCE"), + Symbol::from("BTCUSDT"), + Currency::from("BTC"), + Currency::from("USDT"), + 2, 6, + Price::from("0.01"), + Quantity::from("0.000001"), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.001").unwrap(), + Decimal::from_str("0.001").unwrap(), + None, + Some(Quantity::from("9000")), + Some(Quantity::from("0.000001")), + Some(Price::from("1000000")), + Some(Price::from("0.01")), + ).unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +/////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests{ + use rstest::rstest; + use crate::instruments::currency_pair::CurrencyPair; + use crate::instruments::currency_pair::stubs::currency_pair_btcusdt; + + #[rstest] + fn test_equality(currency_pair_btcusdt: CurrencyPair) { + let cloned = currency_pair_btcusdt.clone(); + assert_eq!(currency_pair_btcusdt, cloned) + } +} \ No newline at end of file diff --git a/nautilus_core/model/src/python/instruments/currency_pair.rs b/nautilus_core/model/src/python/instruments/currency_pair.rs new file mode 100644 index 000000000000..03d6718da4dc --- /dev/null +++ b/nautilus_core/model/src/python/instruments/currency_pair.rs @@ -0,0 +1,136 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + + +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use pyo3::basic::CompareOp; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use rust_decimal::Decimal; +use rust_decimal::prelude::ToPrimitive; +use nautilus_core::python::serialization::from_dict_pyo3; +use nautilus_core::python::to_pyvalue_err; +use crate::identifiers::instrument_id::InstrumentId; +use crate::identifiers::symbol::Symbol; +use crate::instruments::currency_pair::CurrencyPair; +use crate::types::currency::Currency; +use crate::types::price::Price; +use crate::types::quantity::Quantity; + +#[pymethods] +impl CurrencyPair{ + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + id: InstrumentId, + raw_symbol: Symbol, + base_currency: Currency, + quote_currency: Currency, + price_precision: u8, + size_precision: u8, + price_increment: Price, + size_increment: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_price: Option, + min_price: Option, + )->PyResult{ + Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + price_precision, + size_precision, + price_increment, + size_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price + ).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"), + } + } + + fn __hash__(&self) -> isize { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as isize + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(CurrencyPair))?; + dict.set_item("id", self.id.to_string())?; + dict.set_item("raw_symbol", self.raw_symbol.to_string())?; + dict.set_item("base_currency", self.base_currency.code.to_string())?; + dict.set_item("quote_currency", self.quote_currency.code.to_string())?; + dict.set_item("price_precision", self.price_precision)?; + dict.set_item("size_precision", self.size_precision)?; + dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("margin_init", self.margin_init.to_f64())?; + dict.set_item("margin_maint", self.margin_maint.to_f64())?; + dict.set_item("maker_fee", self.margin_init.to_f64())?; + dict.set_item("taker_fee", self.margin_init.to_f64())?; + match self.lot_size { + Some(value) => dict.set_item("lot_size", value.to_string())?, + None => dict.set_item("lot_size", py.None())?, + } + match self.max_quantity { + Some(value) => dict.set_item("max_quantity", value.to_string())?, + None => dict.set_item("max_quantity", py.None())?, + } + match self.min_quantity { + Some(value) => dict.set_item("min_quantity", value.to_string())?, + None => dict.set_item("min_quantity", py.None())?, + } + match self.max_price { + Some(value) => dict.set_item("max_price", value.to_string())?, + None => dict.set_item("max_price", py.None())?, + } + match self.min_price { + Some(value) => dict.set_item("min_price", value.to_string())?, + None => dict.set_item("min_price", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 02118ec9c5b5..2b84ce624d5e 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -15,3 +15,4 @@ pub mod crypto_future; pub mod crypto_perpetual; +pub mod currency_pair; diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 1d6d53327bbe..1e71733bd546 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -590,7 +590,7 @@ class Quantity: class CryptoFuture: ... class CryptoPerpetual: ... -class CurrenyPair: ... +class CurrencyPair: ... class Equity: ... class FuturesContract: ... class OptionsContract: ... diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index 47280fe7c32d..95050ed97d8f 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -20,6 +20,7 @@ from nautilus_trader.core.nautilus_pyo3 import CryptoFuture from nautilus_trader.core.nautilus_pyo3 import CryptoPerpetual +from nautilus_trader.core.nautilus_pyo3 import CurrencyPair from nautilus_trader.core.nautilus_pyo3 import InstrumentId from nautilus_trader.core.nautilus_pyo3 import Money from nautilus_trader.core.nautilus_pyo3 import Price @@ -83,3 +84,25 @@ def btcusdt_future_binance(expiry: pd.Timestamp | None = None) -> CryptoFuture: Price.from_str("1000000.0"), Price.from_str("0.01"), ) + + @staticmethod + def btcusdt_binance() -> CurrencyPair: + return CurrencyPair( # type: ignore + InstrumentId.from_str("BTCUSDT.BINANCE"), + Symbol("BTCUSDT"), + TestTypesProviderPyo3.currency_btc(), + TestTypesProviderPyo3.currency_usdt(), + 2, + 6, + Price.from_str("0.01"), + Quantity.from_str("0.000001"), + 0.0, + 0.0, + 0.001, + 0.001, + None, + Quantity.from_str("9000"), + Quantity.from_str("0.00001"), + Price.from_str("1000000"), + Price.from_str("0.01") + ) diff --git a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py index 5ef25dccb9ff..25bd6e5605d4 100644 --- a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py @@ -20,7 +20,7 @@ crypto_future_btcusdt = TestInstrumentProviderPyo3.btcusdt_future_binance() -class TestCryptoFuture: +class TestCryptoFuturePyo3: def test_equality(self): item_1 = TestInstrumentProviderPyo3.btcusdt_future_binance() item_2 = TestInstrumentProviderPyo3.btcusdt_future_binance() diff --git a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py index 47db75f75d9a..ee0186b6f30b 100644 --- a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py @@ -20,7 +20,7 @@ crypto_perpetual_ethusdt_perp = TestInstrumentProviderPyo3.ethusdt_perp_binance() -class TestCryptoPerpetual: +class TestCryptoPerpetualPyo3: def test_equality(self): item_1 = TestInstrumentProviderPyo3.ethusdt_perp_binance() item_2 = TestInstrumentProviderPyo3.ethusdt_perp_binance() diff --git a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py new file mode 100644 index 000000000000..0b6aeb7b9800 --- /dev/null +++ b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py @@ -0,0 +1,55 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3 +from nautilus_trader.core.nautilus_pyo3 import CurrencyPair + +btcusdt_binance = TestInstrumentProviderPyo3.btcusdt_binance() + +class TestCurrencyPairPyo3: + + def test_equality(self): + item_1 = TestInstrumentProviderPyo3.btcusdt_binance() + item_2 = TestInstrumentProviderPyo3.btcusdt_binance() + assert item_1 == item_2 + + def test_hash(self): + assert hash(btcusdt_binance) == hash(btcusdt_binance) + + + def test_to_dict(self): + dict = btcusdt_binance.to_dict() + assert CurrencyPair.from_dict(dict) == btcusdt_binance + assert dict == { + "type": "CurrencyPair", + "id": "BTCUSDT.BINANCE", + "raw_symbol": "BTCUSDT", + "base_currency": "BTC", + "quote_currency": "USDT", + "price_precision": 2, + "size_precision": 6, + "price_increment": "0.01", + "size_increment": "0.000001", + "margin_maint": 0.0, + "margin_init": 0.0, + "maker_fee": 0.0, + "taker_fee": 0.0, + "lot_size": None, + "max_quantity": "9000", + "min_quantity": "0.00001", + "min_price": "0.01", + "max_price": "1000000", + } +