diff --git a/Cargo.lock b/Cargo.lock index 2fd8f06..f978a8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1136,7 +1136,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lfest" -version = "0.138.4" +version = "0.139.0" dependencies = [ "assert2", "const-decimal", @@ -2203,9 +2203,9 @@ dependencies = [ [[package]] name = "trade_aggregation" -version = "13.0.1" +version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba52d7dbeef22576e443d0df8f010e11764a49714769e26257ac3f40265e62f" +checksum = "b253fcd43f38707eb1dfd969795d890799a7306abb60057e4884d6856ebca1e4" dependencies = [ "csv", "thiserror 1.0.69", @@ -2214,12 +2214,13 @@ dependencies = [ [[package]] name = "trade_aggregation_derive" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3baef2cac2126ddb2105aed8533e8dedfcedb86a013924e4e3a2401878bad6f" +checksum = "8dce12c089f7e9e49b34f1e6728219be9300ad6512f3823736df8bba316d37c5" dependencies = [ + "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.119", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6d6087e..9d28b69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lfest" -version = "0.138.4" +version = "0.139.0" authors = ["MathisWellmann "] edition = "2024" license-file = "LICENSE" @@ -209,7 +209,7 @@ glidesort = "0.1" csv = "1" mutants = "0.0.4" -trade_aggregation = { version = "13", optional = true } +trade_aggregation = { version = "14", optional = true } [dev-dependencies] rand = "0.10" diff --git a/src/trade_aggregation.rs b/src/trade_aggregation.rs index 64eac31..bb852ed 100644 --- a/src/trade_aggregation.rs +++ b/src/trade_aggregation.rs @@ -1,16 +1,40 @@ -use trade_aggregation::TakerTrade; +//! Aggregate lfest trades into candles with the `trade_aggregation` crate. +//! +//! A [`Trade`] is a [`TakerTrade`] with a nanosecond timestamp, and the unit of its size +//! follows the currency of its quantity, so `By::Base` and `By::Quote` sum base volumes +//! and notionals for linear and inverse contracts alike. + +use trade_aggregation::{ + SizeUnit, + TakerTrade, + TimestampResolution, + Timestamped, +}; use crate::prelude::*; -impl TakerTrade for Trade +impl Timestamped for Trade where I: Mon, BaseOrQuote: Currency, { + const TIMESTAMP_RESOLUTION: TimestampResolution = TimestampResolution::Nanosecond; + #[inline(always)] fn timestamp(&self) -> i64 { self.timestamp_exchange_ns.get() } +} + +impl TakerTrade for Trade +where + I: Mon, + BaseOrQuote: Currency, +{ + const SIZE_UNIT: SizeUnit = match BaseOrQuote::KIND { + CurrencyKind::Base => SizeUnit::Base, + CurrencyKind::Quote => SizeUnit::Quote, + }; #[inline(always)] fn price(&self) -> f64 { @@ -26,60 +50,40 @@ where } } -impl From> for trade_aggregation::Trade -where - I: Mon, - BaseOrQuote: Currency, -{ - #[inline] - fn from(trade: Trade) -> Self { - Self { - timestamp: trade.timestamp_exchange_ns.get(), - price: trade.price.into(), - size: as TakerTrade>::size(&trade), - } - } -} - #[cfg(test)] mod test { use super::*; #[test] - fn taker_trade() { + fn a_linear_trade_has_base_sizes_and_nanosecond_timestamps() { let trade = Trade { price: QuoteCurrency::::new(100, 0), quantity: BaseCurrency::new(5, 0), - side: Side::Buy, + side: Side::Sell, timestamp_exchange_ns: 1.into(), }; - assert_eq!(trade.size(), 5.0); - assert_eq!( - > as TakerTrade>::size(&trade), - 5.0 - ); - assert_eq!( - > as TakerTrade>::price(&trade), - 100.0 - ); - assert_eq!( - > as TakerTrade>::price(&trade), - 100.0 - ); - assert_eq!(trade.timestamp(), 1_i64); - assert_eq!( - > as TakerTrade>::timestamp(&trade), - 1_i64 - ); + type T = Trade>; + assert_eq!(T::TIMESTAMP_RESOLUTION, TimestampResolution::Nanosecond); + assert_eq!(T::SIZE_UNIT, SizeUnit::Base); + assert_eq!(Timestamped::timestamp(&trade), 1); + assert_eq!(TakerTrade::price(&trade), 100.0); + assert_eq!(TakerTrade::size(&trade), -5.0); + assert_eq!(trade.base_size(), -5.0); + assert_eq!(trade.quote_size(), -500.0); + } - let t: trade_aggregation::Trade = trade.into(); - assert_eq!( - t, - trade_aggregation::Trade { - price: 100.0, - size: 5.0, - timestamp: 1, - } - ); + #[test] + fn an_inverse_trade_has_quote_sizes() { + let trade = Trade { + price: QuoteCurrency::::new(100, 0), + quantity: QuoteCurrency::new(500, 0), + side: Side::Buy, + timestamp_exchange_ns: 2.into(), + }; + type T = Trade>; + assert_eq!(T::SIZE_UNIT, SizeUnit::Quote); + assert_eq!(TakerTrade::size(&trade), 500.0); + assert_eq!(trade.base_size(), 5.0); + assert_eq!(trade.quote_size(), 500.0); } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 36a74e7..102cc0f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -41,6 +41,7 @@ pub use side::Side; pub use smol_currency::{ BaseCurrency, Currency, + CurrencyKind, MarginCurrency, Mon, QuoteCurrency, diff --git a/src/types/smol_currency/base_currency.rs b/src/types/smol_currency/base_currency.rs index 2660a74..d9073a5 100644 --- a/src/types/smol_currency/base_currency.rs +++ b/src/types/smol_currency/base_currency.rs @@ -16,6 +16,7 @@ use num_traits::{ use super::{ Currency, + CurrencyKind, MarginCurrency, Mon, QuoteCurrency, @@ -120,6 +121,8 @@ where // Generic over the decimal points in paired currency. type PairedCurrency = QuoteCurrency; + const KIND: CurrencyKind = CurrencyKind::Base; + #[inline] fn convert_from(units: Self::PairedCurrency, price_per_unit: QuoteCurrency) -> Self { assert2::debug_assert!(price_per_unit > Zero::zero()); diff --git a/src/types/smol_currency/mod.rs b/src/types/smol_currency/mod.rs index 1c33b35..c89a486 100644 --- a/src/types/smol_currency/mod.rs +++ b/src/types/smol_currency/mod.rs @@ -28,6 +28,16 @@ impl Mon for i32 {} impl Mon for i64 {} impl Mon for i128 {} +/// Whether a [`Currency`] is the base or the quote currency of a symbol, +/// e.g. BTC or USD of BTCUSD. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CurrencyKind { + /// The base currency, e.g. BTC of BTCUSD: [`BaseCurrency`]. + Base, + /// The quote currency, e.g. USD of BTCUSD: [`QuoteCurrency`]. + Quote, +} + /// A currency must be marked as it can be either a `Base` or `Quote` currency. /// /// # Generics: @@ -58,6 +68,10 @@ where /// The paired currency in the `Symbol` with generic decimal precision `DP`. type PairedCurrency: Currency; + /// Whether this is the base or the quote currency, + /// so that generic code can tell the unit of a quantity at compile time. + const KIND: CurrencyKind; + /// Convert from one currency to another at a given price per unit. fn convert_from(units: Self::PairedCurrency, price_per_unit: QuoteCurrency) -> Self; } @@ -66,6 +80,16 @@ where mod tests { use super::*; + #[test] + fn currencies_know_their_kind() { + assert_eq!(BaseCurrency::::KIND, CurrencyKind::Base); + assert_eq!(QuoteCurrency::::KIND, CurrencyKind::Quote); + assert_eq!( + as Currency>::PairedCurrency::KIND, + CurrencyKind::Quote + ); + } + #[test] fn convert_base_to_quote() { // 0.5 BTC @ 100 USD = 50 USD diff --git a/src/types/smol_currency/quote_currency.rs b/src/types/smol_currency/quote_currency.rs index 28c25c5..c4808fa 100644 --- a/src/types/smol_currency/quote_currency.rs +++ b/src/types/smol_currency/quote_currency.rs @@ -17,6 +17,7 @@ use num_traits::{ use super::{ BaseCurrency, Currency, + CurrencyKind, MarginCurrency, Mon, }; @@ -147,6 +148,8 @@ where { type PairedCurrency = BaseCurrency; + const KIND: CurrencyKind = CurrencyKind::Quote; + #[inline] fn convert_from(units: Self::PairedCurrency, price_per_unit: QuoteCurrency) -> Self { assert2::debug_assert!(price_per_unit >= Zero::zero());