From b2ee47f160f3d94fa9feb53264eda7aa3eb3d5e5 Mon Sep 17 00:00:00 2001 From: MathisWellmann Date: Thu, 12 Nov 2026 14:21:34 +0000 Subject: [PATCH] trade_aggregation: support 14, nanosecond timestamps and size units, 0.139.0 trade_aggregation 14 takes the timestamp resolution and the currency of the trade size from the trade type. The old impl left the resolution at its default of milliseconds although the timestamps are in nanoseconds, so the parts of trade_aggregation that read it from the trade (Adaptive, MinDuration, MaxDuration, TimeVelocity) were off by a factor of 10^6. Trade now implements Timestamped with TimestampResolution::Nanosecond, and TakerTrade with SIZE_UNIT from the currency of its quantity, so By::Base and By::Quote sum base volumes and notionals for linear and inverse contracts. For that, Currency gets a KIND constant (CurrencyKind::Base or ::Quote), which generic code can also use elsewhere. The From for trade_aggregation::Trade conversion is removed: that type now has millisecond timestamps and quote sizes, so the conversion would lose precision, and a Trade can be aggregated directly. Breaking (new required Currency constant, removed conversion), so the version is 0.139.0. --- Cargo.lock | 13 +-- Cargo.toml | 4 +- src/trade_aggregation.rs | 96 ++++++++++++----------- src/types/mod.rs | 1 + src/types/smol_currency/base_currency.rs | 3 + src/types/smol_currency/mod.rs | 24 ++++++ src/types/smol_currency/quote_currency.rs | 3 + 7 files changed, 90 insertions(+), 54 deletions(-) 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());