Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CurrencyPair in rust with pyo3 #1313

Merged
merged 1 commit into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 73 additions & 16 deletions nautilus_core/model/src/instruments/currency_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

use std::hash::{Hash, Hasher};

use anyhow::Result;
use pyo3::prelude::*;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
Expand All @@ -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<Quantity>,
pub max_quantity: Option<Quantity>,
pub min_quantity: Option<Quantity>,
pub max_price: Option<Price>,
pub min_price: Option<Price>,
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,
Expand All @@ -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<Quantity>,
max_quantity: Option<Quantity>,
min_quantity: Option<Quantity>,
max_price: Option<Price>,
min_price: Option<Price>,
margin_init: Decimal,
margin_maint: Decimal,
maker_fee: Decimal,
taker_fee: Decimal,
) -> Self {
Self {
) -> Result<Self> {
Ok(Self {
id,
raw_symbol,
quote_currency,
Expand All @@ -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,
}
})
}
}

Expand Down Expand Up @@ -202,3 +202,60 @@ 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, symbol::Symbol},
instruments::currency_pair::CurrencyPair,
types::{currency::Currency, price::Price, 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::{stubs::currency_pair_btcusdt, CurrencyPair};

#[rstest]
fn test_equality(currency_pair_btcusdt: CurrencyPair) {
let cloned = currency_pair_btcusdt.clone();
assert_eq!(currency_pair_btcusdt, cloned)
}
}
133 changes: 133 additions & 0 deletions nautilus_core/model/src/python/instruments/currency_pair.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// -------------------------------------------------------------------------------------------------
// 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,
hash::{Hash, Hasher},
};

use nautilus_core::python::{serialization::from_dict_pyo3, to_pyvalue_err};
use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
use rust_decimal::{prelude::ToPrimitive, Decimal};

use crate::{
identifiers::{instrument_id::InstrumentId, symbol::Symbol},
instruments::currency_pair::CurrencyPair,
types::{currency::Currency, price::Price, 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<Quantity>,
max_quantity: Option<Quantity>,
min_quantity: Option<Quantity>,
max_price: Option<Price>,
min_price: Option<Price>,
) -> PyResult<Self> {
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<PyAny> {
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<PyDict>) -> PyResult<Self> {
from_dict_pyo3(py, values)
}

#[pyo3(name = "to_dict")]
fn py_to_dict(&self, py: Python<'_>) -> PyResult<PyObject> {
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())
}
}
1 change: 1 addition & 0 deletions nautilus_core/model/src/python/instruments/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@

pub mod crypto_future;
pub mod crypto_perpetual;
pub mod currency_pair;
2 changes: 1 addition & 1 deletion nautilus_trader/core/nautilus_pyo3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ class Quantity:

class CryptoFuture: ...
class CryptoPerpetual: ...
class CurrenyPair: ...
class CurrencyPair: ...
class Equity: ...
class FuturesContract: ...
class OptionsContract: ...
Expand Down
23 changes: 23 additions & 0 deletions nautilus_trader/test_kit/rust/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
54 changes: 54 additions & 0 deletions tests/unit_tests/model/instruments/test_currency_pair_pyo3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -------------------------------------------------------------------------------------------------
# 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.core.nautilus_pyo3 import CurrencyPair
from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3


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",
}