Skip to content
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
13 changes: 7 additions & 6 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "lfest"
version = "0.138.4"
version = "0.139.0"
authors = ["MathisWellmann <wellmannmathis@gmail.com>"]
edition = "2024"
license-file = "LICENSE"
Expand Down Expand Up @@ -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"
Expand Down
96 changes: 50 additions & 46 deletions src/trade_aggregation.rs
Original file line number Diff line number Diff line change
@@ -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<I, const D: u8, BaseOrQuote> TakerTrade for Trade<I, D, BaseOrQuote>
impl<I, const D: u8, BaseOrQuote> Timestamped for Trade<I, D, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
const TIMESTAMP_RESOLUTION: TimestampResolution = TimestampResolution::Nanosecond;

#[inline(always)]
fn timestamp(&self) -> i64 {
self.timestamp_exchange_ns.get()
}
}

impl<I, const D: u8, BaseOrQuote> TakerTrade for Trade<I, D, BaseOrQuote>
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
const SIZE_UNIT: SizeUnit = match BaseOrQuote::KIND {
CurrencyKind::Base => SizeUnit::Base,
CurrencyKind::Quote => SizeUnit::Quote,
};

#[inline(always)]
fn price(&self) -> f64 {
Expand All @@ -26,60 +50,40 @@ where
}
}

impl<I, const D: u8, BaseOrQuote> From<Trade<I, D, BaseOrQuote>> for trade_aggregation::Trade
where
I: Mon<D>,
BaseOrQuote: Currency<I, D>,
{
#[inline]
fn from(trade: Trade<I, D, BaseOrQuote>) -> Self {
Self {
timestamp: trade.timestamp_exchange_ns.get(),
price: trade.price.into(),
size: <Trade<I, D, BaseOrQuote> 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::<i64, 5>::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!(
<Trade<i64, 5, BaseCurrency<i64, 5>> as TakerTrade>::size(&trade),
5.0
);
assert_eq!(
<Trade<i64, 5, BaseCurrency<i64, 5>> as TakerTrade>::price(&trade),
100.0
);
assert_eq!(
<Trade<i64, 5, BaseCurrency<i64, 5>> as TakerTrade>::price(&trade),
100.0
);
assert_eq!(trade.timestamp(), 1_i64);
assert_eq!(
<Trade<i64, 5, BaseCurrency<i64, 5>> as TakerTrade>::timestamp(&trade),
1_i64
);
type T = Trade<i64, 5, BaseCurrency<i64, 5>>;
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::<i64, 5>::new(100, 0),
quantity: QuoteCurrency::new(500, 0),
side: Side::Buy,
timestamp_exchange_ns: 2.into(),
};
type T = Trade<i64, 5, QuoteCurrency<i64, 5>>;
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);
}
}
1 change: 1 addition & 0 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub use side::Side;
pub use smol_currency::{
BaseCurrency,
Currency,
CurrencyKind,
MarginCurrency,
Mon,
QuoteCurrency,
Expand Down
3 changes: 3 additions & 0 deletions src/types/smol_currency/base_currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use num_traits::{

use super::{
Currency,
CurrencyKind,
MarginCurrency,
Mon,
QuoteCurrency,
Expand Down Expand Up @@ -120,6 +121,8 @@ where
// Generic over the decimal points in paired currency.
type PairedCurrency = QuoteCurrency<I, D>;

const KIND: CurrencyKind = CurrencyKind::Base;

#[inline]
fn convert_from(units: Self::PairedCurrency, price_per_unit: QuoteCurrency<I, D>) -> Self {
assert2::debug_assert!(price_per_unit > Zero::zero());
Expand Down
24 changes: 24 additions & 0 deletions src/types/smol_currency/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ impl<const D: u8> Mon<D> for i32 {}
impl<const D: u8> Mon<D> for i64 {}
impl<const D: u8> Mon<D> 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:
Expand Down Expand Up @@ -58,6 +68,10 @@ where
/// The paired currency in the `Symbol` with generic decimal precision `DP`.
type PairedCurrency: Currency<I, D, PairedCurrency = Self>;

/// 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<I, D>) -> Self;
}
Expand All @@ -66,6 +80,16 @@ where
mod tests {
use super::*;

#[test]
fn currencies_know_their_kind() {
assert_eq!(BaseCurrency::<i64, 5>::KIND, CurrencyKind::Base);
assert_eq!(QuoteCurrency::<i64, 5>::KIND, CurrencyKind::Quote);
assert_eq!(
<BaseCurrency<i64, 5> as Currency<i64, 5>>::PairedCurrency::KIND,
CurrencyKind::Quote
);
}

#[test]
fn convert_base_to_quote() {
// 0.5 BTC @ 100 USD = 50 USD
Expand Down
3 changes: 3 additions & 0 deletions src/types/smol_currency/quote_currency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use num_traits::{
use super::{
BaseCurrency,
Currency,
CurrencyKind,
MarginCurrency,
Mon,
};
Expand Down Expand Up @@ -147,6 +148,8 @@ where
{
type PairedCurrency = BaseCurrency<I, D>;

const KIND: CurrencyKind = CurrencyKind::Quote;

#[inline]
fn convert_from(units: Self::PairedCurrency, price_per_unit: QuoteCurrency<I, D>) -> Self {
assert2::debug_assert!(price_per_unit >= Zero::zero());
Expand Down
Loading