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
19 changes: 17 additions & 2 deletions src/price_level/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2288,7 +2288,16 @@ impl PriceLevel {
}
}

/// Serializable representation of a price level for easier data transfer and storage
/// Serializable representation of a price level for easier data transfer and storage.
///
/// The `orders` vector is materialized in **queue-consumption order**
/// (ascending insertion sequence — exactly as `match_order` sweeps), and
/// [`TryFrom<PriceLevelData>`](PriceLevel#impl-TryFrom<PriceLevelData>-for-PriceLevel)
/// re-admits in vector order, so a `PriceLevelData` round-trip preserves
/// price-time (FIFO) priority just like the checksum-protected snapshot
/// package. Unlike the package, this plain representation carries no checksum
/// and no statistics — prefer [`PriceLevel::snapshot_package`] for
/// persistence.
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PriceLevelData {
Expand All @@ -2311,8 +2320,14 @@ impl From<&PriceLevel> for PriceLevelData {
visible_quantity: price_level.visible_quantity(),
hidden_quantity: price_level.hidden_quantity(),
order_count: price_level.order_count(),
// Consumption (insertion-sequence) order, NOT the unordered DashMap
// iteration: `TryFrom<PriceLevelData>` re-admits in vector order,
// so this is what makes the round-trip preserve price-time / FIFO
// priority (issue #131) — the same contract the snapshot package
// has kept since issue #109.
orders: price_level
.iter_orders()
.snapshot_by_insertion_seq()
.into_iter()
.map(|order_arc| *order_arc)
.collect(),
}
Expand Down
45 changes: 45 additions & 0 deletions src/price_level/tests/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5687,6 +5687,51 @@ mod tests {
);
}

#[test]
fn test_price_level_data_roundtrip_preserves_demotion_order() {
// Issue #131: the plain serde path (PriceLevelData) previously built
// its orders from the UNORDERED DashMap iterator, so a round-trip
// scrambled price-time priority — the exact property the snapshot
// package has preserved since #109. It now materializes in
// consumption order and TryFrom re-admits in vector order.
let level = PriceLevel::new(10_000);
level
.add_order(create_standard_order(1, 10_000, 100))
.expect("admission");
level
.add_order(create_standard_order(2, 10_000, 100))
.expect("admission");
level
.add_order(create_standard_order(3, 10_000, 100))
.expect("admission");
// Upsize maker 1: demoted to the tail with its original timestamp,
// so only consumption order (not timestamp order) captures it.
let updated = level
.update_order(OrderUpdate::UpdateQuantity {
order_id: Id::from_u64(1),
new_quantity: Quantity::new(150),
})
.expect("upsize update should succeed");
assert!(updated.is_some(), "maker 1 must still be present");

let data = PriceLevelData::from(&level);
let json = serde_json::to_string(&data).expect("serialize PriceLevelData");
let decoded: PriceLevelData =
serde_json::from_str(&json).expect("deserialize PriceLevelData");
let restored = PriceLevel::try_from(decoded).expect("re-admission must succeed");

let restored_ids: Vec<Id> = restored
.snapshot_by_insertion_seq()
.iter()
.map(|o| o.id())
.collect();
assert_eq!(
restored_ids,
vec![Id::from_u64(2), Id::from_u64(3), Id::from_u64(1)],
"PriceLevelData round-trip must preserve the demoted consumption order"
);
}

#[test]
fn test_snapshot_restore_preserves_upsize_demotion() {
// Issue #109: sizing an order up demotes it to the back of the queue
Expand Down
Loading