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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pricelevel"
version = "0.3.1"
version = "0.4.0"
edition = "2024"
authors = ["Joaquin Bejar <jb@taunais.com>"]
description = "A high-performance, lock-free price level implementation for limit order books in Rust. This library provides the building blocks for creating efficient trading systems with support for multiple order types and concurrent access patterns."
Expand Down Expand Up @@ -36,6 +36,7 @@ crossbeam = "0.8"
uuid = { version = "1.18", features = ["v4", "v5", "serde"] }
ulid = { version = "1.2", features = ["serde"] }
dashmap = "6.1"
sha2 = "0.10"


[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ The performance characteristics demonstrate that the `pricelevel` library is sui

**Joaquín Béjar García**
- Email: jb@taunais.com
- **Telegram**: [@joaquin_bejar](https://t.me/joaquin_bejar)
- GitHub: [joaquinbejar](https://github.com/joaquinbejar)

We appreciate your interest and look forward to your contributions!
Expand Down
1 change: 1 addition & 0 deletions README.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@

**Joaquín Béjar García**
- Email: jb@taunais.com
- **Telegram**: [@joaquin_bejar](https://t.me/joaquin_bejar)
- GitHub: [joaquinbejar](https://github.com/joaquinbejar)

We appreciate your interest and look forward to your contributions!
Expand Down
38 changes: 38 additions & 0 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ pub enum PriceLevelError {
/// Explanation of why the operation is invalid
message: String,
},

/// Error raised when serialization of internal data structures fails.
SerializationError {
/// Descriptive message with the serialization failure details
message: String,
},

/// Error raised when deserialization of external data into internal structures fails.
DeserializationError {
/// Descriptive message with the deserialization failure details
message: String,
},

/// Error raised when a checksum validation fails while restoring a snapshot.
ChecksumMismatch {
/// The checksum that was expected according to the serialized payload
expected: String,
/// The checksum that was computed from the provided payload
actual: String,
},
}
impl Display for PriceLevelError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
Expand All @@ -78,6 +98,15 @@ impl Display for PriceLevelError {
PriceLevelError::InvalidOperation { message } => {
write!(f, "Invalid operation: {message}")
}
PriceLevelError::SerializationError { message } => {
write!(f, "Serialization error: {message}")
}
PriceLevelError::DeserializationError { message } => {
write!(f, "Deserialization error: {message}")
}
PriceLevelError::ChecksumMismatch { expected, actual } => {
write!(f, "Checksum mismatch: expected {expected}, got {actual}")
}
}
}
}
Expand All @@ -97,6 +126,15 @@ impl Debug for PriceLevelError {
PriceLevelError::InvalidOperation { message } => {
write!(f, "Invalid operation: {message}")
}
PriceLevelError::SerializationError { message } => {
write!(f, "Serialization error: {message}")
}
PriceLevelError::DeserializationError { message } => {
write!(f, "Deserialization error: {message}")
}
PriceLevelError::ChecksumMismatch { expected, actual } => {
write!(f, "Checksum mismatch: expected {expected}, got {actual}")
}
}
}
}
Expand Down
66 changes: 64 additions & 2 deletions src/price_level/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::errors::PriceLevelError;
use crate::execution::{MatchResult, Transaction};
use crate::orders::{OrderId, OrderType, OrderUpdate};
use crate::price_level::order_queue::OrderQueue;
use crate::price_level::{PriceLevelSnapshot, PriceLevelStatistics};
use crate::price_level::{PriceLevelSnapshot, PriceLevelSnapshotPackage, PriceLevelStatistics};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
Expand Down Expand Up @@ -35,6 +35,39 @@ pub struct PriceLevel {
stats: Arc<PriceLevelStatistics>,
}

impl PriceLevel {
/// Reconstructs a price level directly from a snapshot.
pub fn from_snapshot(mut snapshot: PriceLevelSnapshot) -> Result<Self, PriceLevelError> {
snapshot.refresh_aggregates();

let order_count = snapshot.orders.len();
let queue = OrderQueue::from(snapshot.orders.clone());

Ok(Self {
price: snapshot.price,
visible_quantity: AtomicU64::new(snapshot.visible_quantity),
hidden_quantity: AtomicU64::new(snapshot.hidden_quantity),
order_count: AtomicUsize::new(order_count),
orders: queue,
stats: Arc::new(PriceLevelStatistics::new()),
})
}

/// Reconstructs a price level from a checksum-protected snapshot package.
pub fn from_snapshot_package(
package: PriceLevelSnapshotPackage,
) -> Result<Self, PriceLevelError> {
let snapshot = package.into_snapshot()?;
Self::from_snapshot(snapshot)
}

/// Restores a price level from its snapshot JSON representation.
pub fn from_snapshot_json(data: &str) -> Result<Self, PriceLevelError> {
let package = PriceLevelSnapshotPackage::from_json(data)?;
Self::from_snapshot_package(package)
}
}

impl PriceLevel {
/// Create a new price level
pub fn new(price: u64) -> Self {
Expand Down Expand Up @@ -225,6 +258,16 @@ impl PriceLevel {
orders: self.iter_orders(),
}
}

/// Serialize the current price level state into a checksum-protected snapshot package.
pub fn snapshot_package(&self) -> Result<PriceLevelSnapshotPackage, PriceLevelError> {
PriceLevelSnapshotPackage::new(self.snapshot())
}

/// Serialize the current price level state to JSON, including checksum metadata.
pub fn snapshot_to_json(&self) -> Result<String, PriceLevelError> {
self.snapshot_package()?.to_json()
}
}

impl PriceLevel {
Expand Down Expand Up @@ -437,12 +480,31 @@ impl From<&PriceLevel> for PriceLevelData {
orders: price_level
.iter_orders()
.into_iter()
.map(|order_arc| (*order_arc))
.map(|order_arc| *order_arc)
.collect(),
}
}
}

impl From<&PriceLevelSnapshot> for PriceLevel {
fn from(value: &PriceLevelSnapshot) -> Self {
let mut snapshot = value.clone();
snapshot.refresh_aggregates();

let order_count = snapshot.orders.len();
let queue = OrderQueue::from(snapshot.orders.clone());

Self {
price: snapshot.price,
visible_quantity: AtomicU64::new(snapshot.visible_quantity),
hidden_quantity: AtomicU64::new(snapshot.hidden_quantity),
order_count: AtomicUsize::new(order_count),
orders: queue,
stats: Arc::new(PriceLevelStatistics::new()),
}
}
}

impl TryFrom<PriceLevelData> for PriceLevel {
type Error = PriceLevelError;

Expand Down
2 changes: 1 addition & 1 deletion src/price_level/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ mod tests;

pub use level::{PriceLevel, PriceLevelData};
pub use order_queue::OrderQueue;
pub use snapshot::PriceLevelSnapshot;
pub use snapshot::{PriceLevelSnapshot, PriceLevelSnapshotPackage};
pub use statistics::PriceLevelStatistics;
103 changes: 102 additions & 1 deletion src/price_level/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::orders::OrderType;
use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
Expand Down Expand Up @@ -45,6 +46,106 @@ impl PriceLevelSnapshot {
pub fn iter_orders(&self) -> impl Iterator<Item = &Arc<OrderType<()>>> {
self.orders.iter()
}

/// Recomputes aggregate fields (`visible_quantity`, `hidden_quantity`, and `order_count`) based on current orders.
pub fn refresh_aggregates(&mut self) {
self.order_count = self.orders.len();

let mut visible_total: u64 = 0;
let mut hidden_total: u64 = 0;

for order in &self.orders {
visible_total = visible_total.saturating_add(order.visible_quantity());
hidden_total = hidden_total.saturating_add(order.hidden_quantity());
}

self.visible_quantity = visible_total;
self.hidden_quantity = hidden_total;
}
}

/// Format version for checksum-enabled price level snapshots.
pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;

/// Serialized representation of a price level snapshot including checksum validation metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceLevelSnapshotPackage {
/// Version of the serialized snapshot schema to support future migrations.
pub version: u32,
/// Captured snapshot data.
pub snapshot: PriceLevelSnapshot,
/// Hex-encoded checksum used to validate the snapshot integrity.
pub checksum: String,
}

impl PriceLevelSnapshotPackage {
/// Creates a new snapshot package computing the checksum for the provided snapshot.
pub fn new(mut snapshot: PriceLevelSnapshot) -> Result<Self, PriceLevelError> {
snapshot.refresh_aggregates();

let checksum = Self::compute_checksum(&snapshot)?;

Ok(Self {
version: SNAPSHOT_FORMAT_VERSION,
snapshot,
checksum,
})
}

/// Serializes the package to JSON.
pub fn to_json(&self) -> Result<String, PriceLevelError> {
serde_json::to_string(self).map_err(|error| PriceLevelError::SerializationError {
message: error.to_string(),
})
}

/// Deserializes a package from JSON.
pub fn from_json(data: &str) -> Result<Self, PriceLevelError> {
serde_json::from_str(data).map_err(|error| PriceLevelError::DeserializationError {
message: error.to_string(),
})
}

/// Validates the checksum contained in the package against the serialized snapshot data.
pub fn validate(&self) -> Result<(), PriceLevelError> {
if self.version != SNAPSHOT_FORMAT_VERSION {
return Err(PriceLevelError::InvalidOperation {
message: format!(
"Unsupported snapshot version: {} (expected {})",
self.version, SNAPSHOT_FORMAT_VERSION
),
});
}

let computed = Self::compute_checksum(&self.snapshot)?;
if computed != self.checksum {
return Err(PriceLevelError::ChecksumMismatch {
expected: self.checksum.clone(),
actual: computed,
});
}

Ok(())
}

/// Consumes the package after validating the checksum and returns the contained snapshot.
pub fn into_snapshot(self) -> Result<PriceLevelSnapshot, PriceLevelError> {
self.validate()?;
Ok(self.snapshot)
}

fn compute_checksum(snapshot: &PriceLevelSnapshot) -> Result<String, PriceLevelError> {
let payload =
serde_json::to_vec(snapshot).map_err(|error| PriceLevelError::SerializationError {
message: error.to_string(),
})?;

let mut hasher = Sha256::new();
hasher.update(payload);

let checksum_bytes = hasher.finalize();
Ok(format!("{:x}", checksum_bytes))
}
}

impl Serialize for PriceLevelSnapshot {
Expand All @@ -60,7 +161,7 @@ impl Serialize for PriceLevelSnapshot {
state.serialize_field("order_count", &self.order_count)?;

let plain_orders: Vec<OrderType<()>> =
self.orders.iter().map(|arc_order| (**arc_order)).collect();
self.orders.iter().map(|arc_order| **arc_order).collect();

state.serialize_field("orders", &plain_orders)?;

Expand Down
Loading
Loading