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