Skip to content

Commit

Permalink
Add CryptoFuture instrument for Rust (#1276)
Browse files Browse the repository at this point in the history
  • Loading branch information
filipmacek authored Oct 20, 2023
1 parent dcea0cf commit 6e663ce
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 28 deletions.
1 change: 1 addition & 0 deletions nautilus_core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nautilus_core/model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde_json = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
ustr = { workspace = true }
chrono = { workspace = true }
derive_builder = "0.12.0"
evalexpr = "11.1.0"
indexmap = "2.0.2"
Expand Down
261 changes: 233 additions & 28 deletions nautilus_core/model/src/instruments/crypto_future.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,25 @@

#![allow(dead_code)] // Allow for development

use std::hash::{Hash, Hasher};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};

use nautilus_core::time::UnixNanos;
use pyo3::prelude::*;
use rust_decimal::Decimal;
use anyhow::Result;
use nautilus_core::{
python::{serialization::from_dict_pyo3, to_pyvalue_err},
time::UnixNanos,
};
use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
use rust_decimal::{prelude::ToPrimitive, Decimal};
use serde::{Deserialize, Serialize};

use super::Instrument;
use crate::{
enums::{AssetClass, AssetType},
identifiers::{instrument_id::InstrumentId, symbol::Symbol},
types::{currency::Currency, price::Price, quantity::Quantity},
types::{currency::Currency, money::Money, price::Price, quantity::Quantity},
};

#[repr(C)]
Expand All @@ -38,67 +45,75 @@ use crate::{
pub struct CryptoFuture {
pub id: InstrumentId,
pub raw_symbol: Symbol,
pub underlying: String,
pub underlying: Currency,
pub quote_currency: Currency,
pub settlement_currency: Currency,
pub expiration: UnixNanos,
pub currency: Currency,
pub price_precision: u8,
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_notional: Option<Money>,
pub min_notional: Option<Money>,
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 CryptoFuture {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
id: InstrumentId,
raw_symbol: Symbol,
underlying: String,
underlying: Currency,
quote_currency: Currency,
settlement_currency: Currency,
expiration: UnixNanos,
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_notional: Option<Money>,
min_notional: Option<Money>,
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,
underlying,
quote_currency,
settlement_currency,
expiration,
currency,
price_precision,
size_precision,
price_increment,
size_increment,
margin_init,
margin_maint,
maker_fee,
taker_fee,
lot_size,
max_quantity,
min_quantity,
max_notional,
min_notional,
max_price,
min_price,
margin_init,
margin_maint,
maker_fee,
taker_fee,
}
})
}
}

Expand Down Expand Up @@ -134,15 +149,15 @@ impl Instrument for CryptoFuture {
}

fn quote_currency(&self) -> &Currency {
&self.currency
&self.quote_currency
}

fn base_currency(&self) -> Option<&Currency> {
None
}

fn settlement_currency(&self) -> &Currency {
&self.currency
&self.settlement_currency
}

fn is_inverse(&self) -> bool {
Expand Down Expand Up @@ -206,3 +221,193 @@ impl Instrument for CryptoFuture {
self.taker_fee
}
}

#[cfg(feature = "python")]
#[pymethods]
impl CryptoFuture {
#[allow(clippy::too_many_arguments)]
#[new]
fn py_new(
id: InstrumentId,
raw_symbol: Symbol,
underlying: Currency,
quote_currency: Currency,
settlement_currency: Currency,
expiration: UnixNanos,
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_notional: Option<Money>,
min_notional: Option<Money>,
max_price: Option<Price>,
min_price: Option<Price>,
) -> PyResult<Self> {
Self::new(
id,
raw_symbol,
underlying,
quote_currency,
settlement_currency,
expiration,
price_precision,
size_precision,
price_increment,
size_increment,
margin_init,
margin_maint,
maker_fee,
taker_fee,
lot_size,
max_quantity,
min_quantity,
max_notional,
min_notional,
max_price,
min_price,
)
.map_err(to_pyvalue_err)
}

fn __hash__(&self) -> isize {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish() as isize
}

fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
match op {
CompareOp::Eq => self.eq(other).into_py(py),
_ => panic!("Not implemented"),
}
}

#[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!(CryptoPerpetual))?;
dict.set_item("id", self.id.to_string())?;
dict.set_item("raw_symbol", self.raw_symbol.to_string())?;
dict.set_item("underlying", self.underlying.code.to_string())?;
dict.set_item("quote_currency", self.quote_currency.code.to_string())?;
dict.set_item(
"settlement_currency",
self.settlement_currency.code.to_string(),
)?;
dict.set_item("expiration", self.expiration.to_i64())?;
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_notional {
Some(value) => dict.set_item("max_notional", value.to_string())?,
None => dict.set_item("max_notional", py.None())?,
}
match self.min_notional {
Some(value) => dict.set_item("min_notional", value.to_string())?,
None => dict.set_item("min_notional", 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())
}
}

////////////////////////////////////////////////////////////////////////////////
// Stubs
////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
pub mod stubs {
use std::str::FromStr;

use chrono::{TimeZone, Utc};
use nautilus_core::time::UnixNanos;
use rstest::fixture;
use rust_decimal::Decimal;

use crate::{
identifiers::{instrument_id::InstrumentId, symbol::Symbol},
instruments::crypto_future::CryptoFuture,
types::{currency::Currency, money::Money, price::Price, quantity::Quantity},
};

#[fixture]
pub fn crypto_future_btcusdt() -> CryptoFuture {
let expiration = Utc.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap();
CryptoFuture::new(
InstrumentId::from("ETHUSDT-123.BINANCE"),
Symbol::from("BTCUSDT"),
Currency::from("BTC"),
Currency::from("USDT"),
Currency::from("USDT"),
expiration.timestamp_nanos_opt().unwrap() as UnixNanos,
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.0")),
Some(Quantity::from("0.000001")),
None,
Some(Money::new(10.00, Currency::from("USDT")).unwrap()),
Some(Price::from("1000000.00")),
Some(Price::from("0.01")),
)
.unwrap()
}
}

////////////////////////////////////////////////////////////////////////////////
// Tests
////////////////////////////////////////////////////////////////////////////////
#[cfg(test)]
mod tests {
use rstest::rstest;

use super::stubs::*;
use crate::instruments::crypto_future::CryptoFuture;

#[rstest]
fn test_equality(crypto_future_btcusdt: CryptoFuture) {
let cloned = crypto_future_btcusdt.clone();
assert_eq!(crypto_future_btcusdt, cloned);
}
}
Loading

0 comments on commit 6e663ce

Please sign in to comment.