1use crate::types::{
9 SettlementEfficiency, SettlementInstruction, SettlementStatus, ZeroBalanceMetrics,
10};
11use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
22pub struct ZeroBalanceFrequency {
23 metadata: KernelMetadata,
24}
25
26impl Default for ZeroBalanceFrequency {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl ZeroBalanceFrequency {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 metadata: KernelMetadata::batch("clearing/zero-balance", Domain::Clearing)
38 .with_description("Settlement efficiency and zero balance metrics")
39 .with_throughput(50_000)
40 .with_latency_us(100.0),
41 }
42 }
43
44 pub fn calculate_zbf(activity: &[DailyActivity], party_id: &str) -> ZeroBalanceMetrics {
46 if activity.is_empty() {
47 return ZeroBalanceMetrics {
48 party_id: party_id.to_string(),
49 total_days: 0,
50 zero_balance_days: 0,
51 frequency: 0.0,
52 avg_eod_position: 0.0,
53 peak_position: 0,
54 avg_intraday_turnover: 0.0,
55 };
56 }
57
58 let total_days = activity.len() as u32;
59 let zero_balance_days = activity.iter().filter(|a| a.eod_position == 0).count() as u32;
60 let frequency = zero_balance_days as f64 / total_days as f64;
61
62 let avg_eod_position =
63 activity.iter().map(|a| a.eod_position as f64).sum::<f64>() / total_days as f64;
64
65 let peak_position = activity
66 .iter()
67 .map(|a| a.peak_intraday_position)
68 .max()
69 .unwrap_or(0);
70
71 let avg_intraday_turnover = activity
72 .iter()
73 .map(|a| a.intraday_turnover as f64)
74 .sum::<f64>()
75 / total_days as f64;
76
77 ZeroBalanceMetrics {
78 party_id: party_id.to_string(),
79 total_days,
80 zero_balance_days,
81 frequency,
82 avg_eod_position,
83 peak_position,
84 avg_intraday_turnover,
85 }
86 }
87
88 pub fn calculate_efficiency(
90 instructions: &[SettlementInstruction],
91 expected_settlement: &HashMap<u64, u64>, actual_settlement: &HashMap<u64, u64>, ) -> SettlementEfficiency {
94 let total_instructions = instructions.len() as u64;
95
96 let mut on_time = 0u64;
97 let mut late = 0u64;
98 let mut failed = 0u64;
99 let mut total_delay = 0i64;
100 let mut delay_count = 0u64;
101
102 let mut party_data: HashMap<String, Vec<DailyActivity>> = HashMap::new();
104
105 for instr in instructions {
106 match instr.status {
107 SettlementStatus::Settled => {
108 if let (Some(&expected), Some(&actual)) = (
109 expected_settlement.get(&instr.id),
110 actual_settlement.get(&instr.id),
111 ) {
112 if actual <= expected {
113 on_time += 1;
114 } else {
115 late += 1;
116 total_delay += (actual - expected) as i64;
117 delay_count += 1;
118 }
119 } else {
120 on_time += 1; }
122 }
123 SettlementStatus::Failed => {
124 failed += 1;
125 }
126 _ => {}
127 }
128
129 party_data
131 .entry(instr.party_id.clone())
132 .or_default()
133 .push(DailyActivity {
134 date: instr.settlement_date,
135 eod_position: 0, peak_intraday_position: instr.quantity.unsigned_abs() as i64,
137 intraday_turnover: instr.quantity.unsigned_abs() as i64,
138 });
139 }
140
141 let on_time_rate = if total_instructions > 0 {
142 on_time as f64 / total_instructions as f64
143 } else {
144 0.0
145 };
146
147 let avg_delay_seconds = if delay_count > 0 {
148 total_delay as f64 / delay_count as f64
149 } else {
150 0.0
151 };
152
153 let party_metrics: Vec<_> = party_data
155 .iter()
156 .map(|(party_id, activity)| Self::calculate_zbf(activity, party_id))
157 .collect();
158
159 SettlementEfficiency {
160 period_days: 1, total_instructions,
162 on_time_settlements: on_time,
163 late_settlements: late,
164 failed_settlements: failed,
165 on_time_rate,
166 avg_delay_seconds,
167 party_metrics,
168 }
169 }
170
171 pub fn calculate_velocity(
173 instructions: &[SettlementInstruction],
174 period_seconds: u64,
175 ) -> SettlementVelocity {
176 let settled: Vec<_> = instructions
177 .iter()
178 .filter(|i| i.status == SettlementStatus::Settled)
179 .collect();
180
181 if settled.is_empty() || period_seconds == 0 {
182 return SettlementVelocity {
183 instructions_per_second: 0.0,
184 value_per_second: 0.0,
185 securities_per_second: 0.0,
186 peak_rate: 0.0,
187 };
188 }
189
190 let total_value: u64 = settled
191 .iter()
192 .map(|i| i.payment_amount.unsigned_abs())
193 .sum();
194 let total_securities: u64 = settled.iter().map(|i| i.quantity.unsigned_abs()).sum();
195
196 let instructions_per_second = settled.len() as f64 / period_seconds as f64;
197 let value_per_second = total_value as f64 / period_seconds as f64;
198 let securities_per_second = total_securities as f64 / period_seconds as f64;
199
200 let peak_rate = instructions_per_second * 2.0; SettlementVelocity {
204 instructions_per_second,
205 value_per_second,
206 securities_per_second,
207 peak_rate,
208 }
209 }
210
211 pub fn score_parties(instructions: &[SettlementInstruction]) -> Vec<PartyEfficiencyScore> {
213 let mut party_stats: HashMap<String, PartyStats> = HashMap::new();
214
215 for instr in instructions {
216 let stats = party_stats.entry(instr.party_id.clone()).or_default();
217 stats.total += 1;
218
219 match instr.status {
220 SettlementStatus::Settled => stats.settled += 1,
221 SettlementStatus::Failed => stats.failed += 1,
222 SettlementStatus::Partial => stats.partial += 1,
223 _ => stats.pending += 1,
224 }
225 }
226
227 let mut scores: Vec<_> = party_stats
228 .into_iter()
229 .map(|(party_id, stats)| {
230 let settlement_rate = if stats.total > 0 {
231 stats.settled as f64 / stats.total as f64
232 } else {
233 0.0
234 };
235
236 let failure_rate = if stats.total > 0 {
237 stats.failed as f64 / stats.total as f64
238 } else {
239 0.0
240 };
241
242 let score = (settlement_rate * 100.0 - failure_rate * 50.0).max(0.0);
244
245 PartyEfficiencyScore {
246 party_id,
247 total_instructions: stats.total,
248 settled: stats.settled,
249 failed: stats.failed,
250 pending: stats.pending,
251 settlement_rate,
252 efficiency_score: score,
253 }
254 })
255 .collect();
256
257 scores.sort_by(|a, b| b.efficiency_score.partial_cmp(&a.efficiency_score).unwrap());
259
260 scores
261 }
262
263 pub fn calculate_liquidity_usage(
265 instructions: &[SettlementInstruction],
266 available_liquidity: i64,
267 ) -> LiquidityMetrics {
268 let total_value: u64 = instructions
269 .iter()
270 .filter(|i| i.status != SettlementStatus::Failed)
271 .map(|i| i.payment_amount.unsigned_abs())
272 .sum();
273
274 let settled_value: u64 = instructions
275 .iter()
276 .filter(|i| i.status == SettlementStatus::Settled)
277 .map(|i| i.payment_amount.unsigned_abs())
278 .sum();
279
280 let utilization = if available_liquidity > 0 {
281 (total_value as f64 / available_liquidity as f64).min(1.0)
282 } else {
283 0.0
284 };
285
286 let turnover = if available_liquidity > 0 {
287 settled_value as f64 / available_liquidity as f64
288 } else {
289 0.0
290 };
291
292 LiquidityMetrics {
293 total_value_processed: total_value,
294 settled_value,
295 available_liquidity,
296 utilization_rate: utilization,
297 turnover_ratio: turnover,
298 }
299 }
300}
301
302impl GpuKernel for ZeroBalanceFrequency {
303 fn metadata(&self) -> &KernelMetadata {
304 &self.metadata
305 }
306}
307
308#[derive(Debug, Clone)]
310pub struct DailyActivity {
311 pub date: u64,
313 pub eod_position: i64,
315 pub peak_intraday_position: i64,
317 pub intraday_turnover: i64,
319}
320
321#[derive(Debug, Clone)]
323pub struct SettlementVelocity {
324 pub instructions_per_second: f64,
326 pub value_per_second: f64,
328 pub securities_per_second: f64,
330 pub peak_rate: f64,
332}
333
334#[derive(Debug, Clone)]
336pub struct PartyEfficiencyScore {
337 pub party_id: String,
339 pub total_instructions: u64,
341 pub settled: u64,
343 pub failed: u64,
345 pub pending: u64,
347 pub settlement_rate: f64,
349 pub efficiency_score: f64,
351}
352
353#[derive(Debug, Clone)]
355pub struct LiquidityMetrics {
356 pub total_value_processed: u64,
358 pub settled_value: u64,
360 pub available_liquidity: i64,
362 pub utilization_rate: f64,
364 pub turnover_ratio: f64,
366}
367
368#[derive(Default)]
369struct PartyStats {
370 total: u64,
371 settled: u64,
372 failed: u64,
373 partial: u64,
374 pending: u64,
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::types::InstructionType;
381
382 fn create_instruction(id: u64, party: &str, status: SettlementStatus) -> SettlementInstruction {
383 SettlementInstruction {
384 id,
385 party_id: party.to_string(),
386 security_id: "AAPL".to_string(),
387 instruction_type: InstructionType::Deliver,
388 quantity: 100,
389 payment_amount: 15000,
390 currency: "USD".to_string(),
391 settlement_date: 1700172800,
392 status,
393 source_trades: vec![1],
394 }
395 }
396
397 #[test]
398 fn test_zbf_metadata() {
399 let kernel = ZeroBalanceFrequency::new();
400 assert_eq!(kernel.metadata().id, "clearing/zero-balance");
401 assert_eq!(kernel.metadata().domain, Domain::Clearing);
402 }
403
404 #[test]
405 fn test_calculate_zbf() {
406 let activity = vec![
407 DailyActivity {
408 date: 1700000000,
409 eod_position: 0,
410 peak_intraday_position: 1000,
411 intraday_turnover: 5000,
412 },
413 DailyActivity {
414 date: 1700086400,
415 eod_position: 500,
416 peak_intraday_position: 2000,
417 intraday_turnover: 8000,
418 },
419 DailyActivity {
420 date: 1700172800,
421 eod_position: 0,
422 peak_intraday_position: 1500,
423 intraday_turnover: 6000,
424 },
425 ];
426
427 let metrics = ZeroBalanceFrequency::calculate_zbf(&activity, "PARTY_A");
428
429 assert_eq!(metrics.total_days, 3);
430 assert_eq!(metrics.zero_balance_days, 2);
431 assert!((metrics.frequency - 0.666).abs() < 0.01);
432 assert_eq!(metrics.peak_position, 2000);
433 }
434
435 #[test]
436 fn test_empty_activity() {
437 let activity: Vec<DailyActivity> = vec![];
438
439 let metrics = ZeroBalanceFrequency::calculate_zbf(&activity, "PARTY_A");
440
441 assert_eq!(metrics.total_days, 0);
442 assert_eq!(metrics.frequency, 0.0);
443 }
444
445 #[test]
446 fn test_calculate_efficiency() {
447 let instructions = vec![
448 create_instruction(1, "PARTY_A", SettlementStatus::Settled),
449 create_instruction(2, "PARTY_A", SettlementStatus::Settled),
450 create_instruction(3, "PARTY_B", SettlementStatus::Failed),
451 ];
452
453 let expected: HashMap<u64, u64> = [(1, 1700172800), (2, 1700172800)].into_iter().collect();
454 let actual: HashMap<u64, u64> = [(1, 1700172800), (2, 1700259200)].into_iter().collect();
455
456 let efficiency =
457 ZeroBalanceFrequency::calculate_efficiency(&instructions, &expected, &actual);
458
459 assert_eq!(efficiency.total_instructions, 3);
460 assert_eq!(efficiency.on_time_settlements, 1);
461 assert_eq!(efficiency.late_settlements, 1);
462 assert_eq!(efficiency.failed_settlements, 1);
463 }
464
465 #[test]
466 fn test_calculate_velocity() {
467 let instructions = vec![
468 create_instruction(1, "PARTY_A", SettlementStatus::Settled),
469 create_instruction(2, "PARTY_A", SettlementStatus::Settled),
470 create_instruction(3, "PARTY_B", SettlementStatus::Settled),
471 ];
472
473 let velocity = ZeroBalanceFrequency::calculate_velocity(&instructions, 3600); assert!(velocity.instructions_per_second > 0.0);
476 assert!(velocity.value_per_second > 0.0);
477 }
478
479 #[test]
480 fn test_score_parties() {
481 let instructions = vec![
482 create_instruction(1, "PARTY_A", SettlementStatus::Settled),
483 create_instruction(2, "PARTY_A", SettlementStatus::Settled),
484 create_instruction(3, "PARTY_A", SettlementStatus::Failed),
485 create_instruction(4, "PARTY_B", SettlementStatus::Settled),
486 create_instruction(5, "PARTY_B", SettlementStatus::Settled),
487 ];
488
489 let scores = ZeroBalanceFrequency::score_parties(&instructions);
490
491 assert_eq!(scores[0].party_id, "PARTY_B");
493 assert!(scores[0].efficiency_score > scores[1].efficiency_score);
494 }
495
496 #[test]
497 fn test_calculate_liquidity_usage() {
498 let instructions = vec![
499 create_instruction(1, "PARTY_A", SettlementStatus::Settled),
500 create_instruction(2, "PARTY_A", SettlementStatus::Settled),
501 create_instruction(3, "PARTY_B", SettlementStatus::Pending),
502 ];
503
504 let metrics = ZeroBalanceFrequency::calculate_liquidity_usage(&instructions, 100000);
505
506 assert!(metrics.utilization_rate > 0.0);
507 assert!(metrics.utilization_rate <= 1.0);
508 assert!(metrics.turnover_ratio > 0.0);
509 }
510
511 #[test]
512 fn test_velocity_no_settled() {
513 let instructions = vec![
514 create_instruction(1, "PARTY_A", SettlementStatus::Pending),
515 create_instruction(2, "PARTY_A", SettlementStatus::Failed),
516 ];
517
518 let velocity = ZeroBalanceFrequency::calculate_velocity(&instructions, 3600);
519
520 assert_eq!(velocity.instructions_per_second, 0.0);
521 }
522
523 #[test]
524 fn test_all_zero_balance() {
525 let activity: Vec<DailyActivity> = (0..5)
526 .map(|i| DailyActivity {
527 date: 1700000000 + i * 86400,
528 eod_position: 0,
529 peak_intraday_position: 1000,
530 intraday_turnover: 5000,
531 })
532 .collect();
533
534 let metrics = ZeroBalanceFrequency::calculate_zbf(&activity, "PARTY_A");
535
536 assert_eq!(metrics.zero_balance_days, 5);
537 assert!((metrics.frequency - 1.0).abs() < 0.001);
538 }
539}