Skip to main content

sample_app/
sample_app.rs

1//! Sample app: a bid-side depth book built on `subms-treap`.
2//!
3//! A fixed tape of order events is applied to a price-level index, then the
4//! book is read the way a trading system reads it - top of book first, a band
5//! around the touch, a sweep off the top - and finally rebuilt from a sorted
6//! snapshot. Run the base with `cargo run --example sample_app`; add
7//! `--all-features` (or a subset like `--features range-query`) to light up
8//! the optional sections.
9//!
10//! Keys are price levels in integer ticks, values are the resting quantity at
11//! that level. Everything is seeded and the tape is fixed, so the output is
12//! byte-identical on every run.
13//!
14//! * base             - apply a tape, read the ladder, sweep the touch, restore
15//! * range scan       - resting depth within a price band, ascending
16//! * persistent       - version the book so a prior state stays queryable
17//! * merge-split      - partition the ladder at the touch and stitch it back
18//! * concurrent-reads - publish a frozen book to reader threads under writer churn
19
20use subms_treap::Treap;
21
22/// One line of the order tape.
23enum Event {
24    Post(u32, u64),
25    Amend(u32, i64),
26    Cancel(u32),
27}
28
29const SEED: u64 = 0xB1D;
30
31/// A fixed tape. Deterministic input is the point: the printed report below is
32/// reproducible, which a page quoting that output depends on.
33const TAPE: [Event; 14] = [
34    Event::Post(9998, 1_000),
35    Event::Post(10_000, 500),
36    Event::Post(9999, 250),
37    Event::Post(10_001, 100),
38    Event::Post(9997, 750),
39    Event::Post(10_002, 400),
40    Event::Post(9995, 300),
41    Event::Post(9993, 150),
42    Event::Post(9996, 600),
43    Event::Amend(10_000, 150),
44    Event::Amend(10_001, 800),
45    Event::Cancel(9997),
46    Event::Post(9994, 220),
47    Event::Amend(9993, -50),
48];
49
50fn main() {
51    let mut book = apply_tape();
52    report(&book);
53    sweep_the_touch(&mut book);
54    restore_from_snapshot(&book);
55
56    band_depth();
57
58    #[cfg(feature = "persistent")]
59    versioned_book();
60
61    #[cfg(feature = "merge-split")]
62    partition_ladder();
63
64    #[cfg(feature = "concurrent-reads")]
65    published_snapshot();
66}
67
68/// Apply the tape. A post inserts or replaces a level, an amend adjusts the
69/// resting quantity in place through `get_mut` (no re-descent, no priority
70/// redraw), a cancel removes the level.
71fn apply_tape() -> Treap<u32, u64> {
72    println!("== bid-side depth book ==");
73    let book = build_book();
74    println!("  applied {} events -> {} levels", TAPE.len(), book.len());
75    book
76}
77
78fn build_book() -> Treap<u32, u64> {
79    let mut book: Treap<u32, u64> = Treap::with_capacity(SEED, TAPE.len());
80    for event in &TAPE {
81        match event {
82            Event::Post(px, qty) => {
83                book.insert(*px, *qty);
84            }
85            Event::Amend(px, delta) => {
86                if let Some(qty) = book.get_mut(px) {
87                    *qty = qty.saturating_add_signed(*delta);
88                }
89            }
90            Event::Cancel(px) => {
91                book.remove(px);
92            }
93        }
94    }
95    assert_eq!(book.len(), 9);
96    assert_eq!(
97        book.get(&10_000).copied(),
98        Some(650),
99        "amend applied in place"
100    );
101    assert!(!book.contains_key(&9997), "cancelled level is gone");
102    book
103}
104
105/// Read the book the way a trader does: best price first, then the touch and
106/// its neighbours. `iter_rev` walks the ladder high to low; `floor` and
107/// `predecessor` answer "what is at or below this price" without a scan.
108fn report(book: &Treap<u32, u64>) {
109    let (best_px, best_qty) = book.last().map(|(k, v)| (*k, *v)).expect("non-empty");
110    println!(
111        "  best bid {best_px} x {best_qty} | height {} | {} levels",
112        book.height(),
113        book.len()
114    );
115
116    println!("  top 5, best first:");
117    for (px, qty) in book.iter_rev().take(5) {
118        println!("    {px}  {qty:>5}");
119    }
120
121    let inside = book.predecessor(&best_px).map(|(k, _)| *k).unwrap();
122    println!("  next level down: {inside}");
123    assert_eq!(inside, 10_001);
124
125    // A price that is not a resting level still answers, which is the whole
126    // reason for an ordered index over a hash map.
127    let probe = 9_990u32;
128    println!(
129        "  probe {probe}: floor {:?}, ceiling {:?}",
130        book.floor(&probe).map(|(k, _)| *k),
131        book.ceiling(&probe).map(|(k, _)| *k)
132    );
133    assert_eq!(book.floor(&probe), None);
134    assert_eq!(book.ceiling(&probe).map(|(k, _)| *k), Some(9993));
135}
136
137/// Sweep an aggressive sell through the bid side. `pop_last` takes the best
138/// level in expected O(log n) and hands back both key and value, so the fill
139/// loop never re-descends to find the next price.
140fn sweep_the_touch(book: &mut Treap<u32, u64>) {
141    let mut to_fill = 1_200u64;
142    let mut fills = Vec::new();
143    while to_fill > 0 {
144        let Some((px, qty)) = book.pop_last() else {
145            break;
146        };
147        let take = qty.min(to_fill);
148        to_fill -= take;
149        fills.push((px, take));
150        if qty > take {
151            book.insert(px, qty - take); // partial fill, level survives
152        }
153    }
154    println!("  sweep 1200 lots -> {fills:?}");
155    assert_eq!(fills, vec![(10_002, 400), (10_001, 800)]);
156    assert_eq!(book.len(), 8);
157    assert_eq!(
158        book.last().map(|(k, _)| *k),
159        Some(10_001),
160        "partial fill left the level"
161    );
162}
163
164/// End-of-day restore. `collect_in_order` gives a sorted snapshot; `from_sorted`
165/// rebuilds in O(n) instead of paying n rotating inserts.
166fn restore_from_snapshot(book: &Treap<u32, u64>) {
167    let snapshot: Vec<(u32, u64)> = book.iter().map(|(k, v)| (*k, *v)).collect();
168    let restored = Treap::from_sorted(SEED, snapshot.clone()).expect("snapshot is sorted");
169    println!(
170        "  restored {} levels from a sorted snapshot, height {}",
171        restored.len(),
172        restored.height()
173    );
174    let round_tripped: Vec<(u32, u64)> = restored.iter().map(|(k, v)| (*k, *v)).collect();
175    assert_eq!(round_tripped, snapshot);
176
177    // Unsorted input is rejected rather than silently reordered.
178    let bad = Treap::from_sorted(SEED, [(2u32, 1u64), (1, 1)]);
179    assert!(bad.is_err(), "strictly-ascending precondition enforced");
180}
181
182/// Sum resting depth in a price band without
183/// materialising the whole ladder. `range` descends to the low bound in
184/// expected O(log N), then walks only the window in ascending order. Each
185/// bound is independently inclusive, exclusive, or unbounded.
186fn band_depth() {
187    use subms_treap::RangeBound;
188    println!("\n== range-query: depth in a price band ==");
189    let book = build_book();
190    let (lo, hi) = (9_996u32, 10_000u32);
191    let band: Vec<(u32, u64)> = book
192        .range(RangeBound::Inclusive(&lo), RangeBound::Inclusive(&hi))
193        .map(|(k, v)| (*k, *v))
194        .collect();
195    let depth: u64 = band.iter().map(|(_, q)| *q).sum();
196    println!("  [{lo}, {hi}] -> {} levels, {depth} lots", band.len());
197    assert_eq!(
198        band.iter().map(|(k, _)| *k).collect::<Vec<_>>(),
199        vec![9_996, 9_998, 9_999, 10_000]
200    );
201    assert_eq!(depth, 2_500);
202
203    // Exclusive upper bound drops the touch itself.
204    let inside: u64 = book
205        .range(RangeBound::Inclusive(&lo), RangeBound::Exclusive(&hi))
206        .map(|(_, q)| *q)
207        .sum();
208    println!("  same band, exclusive of {hi}: {inside} lots");
209    assert_eq!(inside, 1_850);
210}
211
212/// `persistent` feature: version the book so a prior state stays queryable.
213/// Each `insert` / `remove` returns a NEW book and leaves the receiver
214/// untouched - the shape a risk what-if branch or an audit trail wants.
215#[cfg(feature = "persistent")]
216fn versioned_book() {
217    use subms_treap::PersistentTreap;
218    println!("\n== persistent: versioned book ==");
219    let open: PersistentTreap<u32, u64> = PersistentTreap::new(SEED);
220    let open = open
221        .insert(9_999, 250)
222        .insert(10_000, 500)
223        .insert(10_001, 100);
224
225    // Branch: what does the book look like if the 9999 level fills?
226    let after_fill = open.remove(&9_999);
227    println!(
228        "  open: {} levels, depth@9999 {:?}",
229        open.len(),
230        open.get(&9_999).copied()
231    );
232    println!(
233        "  after fill: {} levels, depth@9999 {:?}",
234        after_fill.len(),
235        after_fill.get(&9_999)
236    );
237    assert_eq!(open.get(&9_999).copied(), Some(250), "prior version intact");
238    assert_eq!(after_fill.get(&9_999), None);
239    assert_eq!((open.len(), after_fill.len()), (3, 2));
240}
241
242/// `merge-split` feature: partition the ladder at the touch in expected
243/// O(log N), then stitch it back. This is the treap's distinguishing
244/// operation - a red-black tree has no cheap equivalent. `merge` requires
245/// every key on the left to be strictly less than every key on the right.
246#[cfg(feature = "merge-split")]
247fn partition_ladder() {
248    use subms_treap::SplittableTreap;
249    println!("\n== merge-split: partition at the touch ==");
250    let mut book: SplittableTreap<u32, u64> = SplittableTreap::new(SEED);
251    for (px, qty) in [
252        (9_996u32, 600u64),
253        (9_998, 1_000),
254        (9_999, 250),
255        (10_000, 650),
256        (10_001, 900),
257        (10_002, 400),
258    ] {
259        book.insert(px, qty);
260    }
261
262    // Everything strictly below 10000 is the resting book; 10000 and above is
263    // the band a marketable order would clear against.
264    let (resting, marketable) = book.split(&10_000);
265    println!(
266        "  below 10000: {} levels | 10000 and up: {} levels",
267        resting.len(),
268        marketable.len()
269    );
270    assert_eq!((resting.len(), marketable.len()), (3, 3));
271    assert_eq!(
272        marketable.collect_in_order().first().map(|(k, _)| **k),
273        Some(10_000)
274    );
275
276    let rejoined = SplittableTreap::merge(resting, marketable);
277    let keys: Vec<u32> = rejoined
278        .collect_in_order()
279        .into_iter()
280        .map(|(k, _)| *k)
281        .collect();
282    println!("  rejoined: {keys:?}");
283    assert_eq!(keys, vec![9_996, 9_998, 9_999, 10_000, 10_001, 10_002]);
284}
285
286/// `concurrent-reads` feature: freeze the book into a shared snapshot and fan
287/// it out to reader threads (market-data / risk consumers) while the writer
288/// keeps applying updates. Every reader sees a stable point-in-time book.
289#[cfg(feature = "concurrent-reads")]
290fn published_snapshot() {
291    use std::thread;
292    use subms_treap::TreapSnapshot;
293    println!("\n== concurrent-reads: published book snapshot ==");
294    let mut book: Treap<u32, u64> = Treap::new(SEED);
295    for px in 9_990..10_010u32 {
296        book.insert(px, (px as u64) * 10);
297    }
298    let snap = TreapSnapshot::from_treap(&book);
299
300    let readers: Vec<_> = (0..4)
301        .map(|_| {
302            let s = snap.clone();
303            thread::spawn(move || s.range(&9_995, &10_004).count())
304        })
305        .collect();
306
307    // Writer churn after the snapshot: readers must not observe it.
308    book.insert(12_345, 1);
309    book.remove(&9_990);
310
311    for r in readers {
312        assert_eq!(
313            r.join().unwrap(),
314            10,
315            "reader sees the frozen 10-level band"
316        );
317    }
318    println!("  4 readers each counted 10 levels in [9995, 10004]");
319    assert!(
320        snap.get(&12_345).is_none(),
321        "snapshot isolated from later writes"
322    );
323    assert_eq!(snap.len(), 20);
324}