1use subms_hyperloglog::HyperLogLog;
12
13const SYMBOLS: [&str; 8] = [
14 "AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX",
15];
16const EVENTS: usize = 200_000;
17
18const COUNTERPARTY_POOL: [u64; 8] = [30_000, 30_000, 900, 700, 60, 40, 15, 9];
21
22struct Event {
23 venue: u8,
24 symbol: usize,
25 account: u64,
26 counterparty: u64,
27 session: u64,
28}
29
30struct Lcg(u64);
32
33impl Lcg {
34 fn next(&mut self) -> u64 {
35 self.0 = self
36 .0
37 .wrapping_mul(6364136223846793005)
38 .wrapping_add(1442695040888963407);
39 self.0 >> 11
40 }
41}
42
43fn tape() -> Vec<Event> {
47 let mut rng = Lcg(0x5eed);
48 (0..EVENTS)
49 .map(|_| {
50 let venue = (rng.next() % 2) as u8;
51 let r = rng.next() % 100;
53 let symbol = if r < 66 {
54 (r % 2) as usize
55 } else {
56 2 + (rng.next() % 6) as usize
57 };
58 let account = if venue == 0 {
60 rng.next() % 30_000
61 } else {
62 20_000 + rng.next() % 30_000
63 };
64 Event {
65 venue,
66 symbol,
67 account,
68 counterparty: (symbol as u64) << 32 | (rng.next() % COUNTERPARTY_POOL[symbol]),
69 session: rng.next() % 40_000,
70 }
71 })
72 .collect()
73}
74
75fn main() {
76 let tape = tape();
77 println!(
78 "tape: {} order events across 2 venues, 8 symbols\n",
79 tape.len()
80 );
81
82 gateway_sessions(&tape);
83 size_from_an_error_budget();
84
85 #[cfg(feature = "sparse")]
86 per_symbol_counterparties(&tape);
87
88 #[cfg(feature = "union-intersect")]
89 cross_venue_overlap(&tape);
90
91 collector_fan_in(&tape);
92}
93
94fn gateway_sessions(tape: &[Event]) {
98 println!("== gateway: distinct sessions this window ==");
99 let mut hll = HyperLogLog::new(14);
100 let mut first_sightings = 0u64;
101 for e in tape {
102 if hll.add_u64(e.session) {
103 first_sightings += 1;
104 }
105 }
106 let est = hll.estimate();
107 println!(" {} messages -> {:.0} distinct sessions", tape.len(), est);
108 println!(
109 " {} registers advanced, {} bytes of state, +/- {:.2}% standard error",
110 first_sightings,
111 hll.state_bytes(),
112 hll.standard_error() * 100.0
113 );
114 assert!(est > 30_000.0 && est < 50_000.0, "~40k sessions, got {est}");
115}
116
117fn size_from_an_error_budget() {
121 println!("\n== sizing: error budget -> byte budget ==");
122 for budget in [0.05, 0.02, 0.01, 0.005] {
123 let p = HyperLogLog::precision_for_standard_error(budget);
124 let hll = HyperLogLog::new(p);
125 println!(
126 " budget {:>5.1}% -> p={:<2} {:>6} bytes, actual {:.5}%",
127 budget * 100.0,
128 p,
129 hll.state_bytes(),
130 hll.standard_error() * 100.0
131 );
132 }
133}
134
135#[cfg(feature = "sparse")]
140fn per_symbol_counterparties(tape: &[Event]) {
141 use subms_hyperloglog::SparseHyperLogLog;
142 println!("\n== risk: distinct counterparties per symbol ==");
143
144 let mut books: Vec<SparseHyperLogLog> = (0..SYMBOLS.len())
145 .map(|_| SparseHyperLogLog::with_threshold(14, 2_000))
146 .collect();
147 for e in tape {
148 books[e.symbol].add_u64(e.counterparty);
149 }
150
151 let mut sparse_bytes = 0usize;
152 for (i, b) in books.iter().enumerate() {
153 println!(
154 " {:<5} {:>7.0} counterparties {:>6} bytes {}",
155 SYMBOLS[i],
156 b.estimate(),
157 b.state_bytes(),
158 if b.is_sparse() { "sparse" } else { "dense" }
159 );
160 sparse_bytes += b.state_bytes();
161 }
162 let dense_bytes = SYMBOLS.len() * 16_384;
163 println!(" total {sparse_bytes} bytes against {dense_bytes} if every name held a dense array");
164 assert!(
165 sparse_bytes < dense_bytes,
166 "sparse must win on the long tail"
167 );
168}
169
170#[cfg(feature = "union-intersect")]
175fn cross_venue_overlap(tape: &[Event]) {
176 use subms_hyperloglog::{estimate_intersect, estimate_union, intersect_error_bound};
177 println!("\n== venues: account reach and overlap ==");
178
179 let mut a = HyperLogLog::new(14);
180 let mut b = HyperLogLog::new(14);
181 for e in tape {
182 if e.venue == 0 {
183 a.add_u64(e.account);
184 } else {
185 b.add_u64(e.account);
186 }
187 }
188 let union = estimate_union(&a, &b).expect("same precision");
189 let inter = estimate_intersect(&a, &b).expect("same precision");
190 let bound = intersect_error_bound(&a, &b).expect("same precision");
191 println!(" venue 0: {:>7.0} accounts", a.estimate());
192 println!(" venue 1: {:>7.0} accounts", b.estimate());
193 println!(" reach: {union:>7.0} (true 50000)");
194 println!(" both: {inter:>7.0} (true 10000) +/- {bound:.0}");
195 assert!(
196 (union - 50_000.0).abs() / 50_000.0 < 0.05,
197 "reach within 5%, got {union}"
198 );
199 assert!(inter > 0.0, "a 10k overlap must survive the subtraction");
200}
201
202fn collector_fan_in(tape: &[Event]) {
206 println!("\n== collector: merge shipped sketches into a firm-wide reach ==");
207
208 let mut per_venue = [HyperLogLog::new(14), HyperLogLog::new(14)];
209 for e in tape {
210 per_venue[e.venue as usize].add_u64(e.account);
211 }
212 let shipped: Vec<Vec<u8>> = per_venue.iter().map(|h| h.to_bytes()).collect();
213 let on_wire: usize = shipped.iter().map(|b| b.len()).sum();
214
215 let mut firm = HyperLogLog::new(14);
216 for bytes in &shipped {
217 let decoded = HyperLogLog::from_bytes(bytes).expect("collector reads its own format");
218 firm.merge(&decoded).expect("same precision");
219 }
220 println!(
221 " {} sketches on the wire, {} bytes total",
222 shipped.len(),
223 on_wire
224 );
225 println!(
226 " raw ids would have been ~{} bytes",
227 tape.len() * core::mem::size_of::<u64>()
228 );
229 println!(" firm-wide reach: {:.0} (true 50000)", firm.estimate());
230 assert!(
231 (firm.estimate() - 50_000.0).abs() / 50_000.0 < 0.05,
232 "merged reach within 5%"
233 );
234}