rustkernel_clearing/
efficiency.rs

1//! Settlement efficiency metrics kernel.
2//!
3//! This module provides settlement efficiency analysis:
4//! - Zero balance frequency calculation
5//! - Settlement timing metrics
6//! - Party efficiency scoring
7
8use crate::types::{
9    SettlementEfficiency, SettlementInstruction, SettlementStatus, ZeroBalanceMetrics,
10};
11use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
12use std::collections::HashMap;
13
14// ============================================================================
15// Zero Balance Frequency Kernel
16// ============================================================================
17
18/// Zero balance frequency kernel.
19///
20/// Calculates settlement efficiency metrics including zero balance frequency.
21#[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    /// Create a new zero balance frequency kernel.
34    #[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    /// Calculate zero balance metrics for a party.
45    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    /// Calculate settlement efficiency from instructions.
89    pub fn calculate_efficiency(
90        instructions: &[SettlementInstruction],
91        expected_settlement: &HashMap<u64, u64>, // instruction_id -> expected timestamp
92        actual_settlement: &HashMap<u64, u64>,   // instruction_id -> actual timestamp
93    ) -> 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        // Track by party for party metrics
103        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; // Assume on-time if no data
121                    }
122                }
123                SettlementStatus::Failed => {
124                    failed += 1;
125                }
126                _ => {}
127            }
128
129            // Aggregate party data (simplified)
130            party_data
131                .entry(instr.party_id.clone())
132                .or_default()
133                .push(DailyActivity {
134                    date: instr.settlement_date,
135                    eod_position: 0, // Would be calculated from actual balances
136                    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        // Calculate party metrics
154        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, // Simplified
161            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    /// Calculate settlement velocity (instructions settled per time period).
172    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        // Calculate peak rate (simplified - would use time buckets)
201        let peak_rate = instructions_per_second * 2.0; // Assume peak is 2x average
202
203        SettlementVelocity {
204            instructions_per_second,
205            value_per_second,
206            securities_per_second,
207            peak_rate,
208        }
209    }
210
211    /// Score parties by settlement efficiency.
212    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                // Efficiency score: 0-100
243                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        // Sort by efficiency score descending
258        scores.sort_by(|a, b| b.efficiency_score.partial_cmp(&a.efficiency_score).unwrap());
259
260        scores
261    }
262
263    /// Calculate liquidity usage metrics.
264    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/// Daily activity for a party.
309#[derive(Debug, Clone)]
310pub struct DailyActivity {
311    /// Date (Unix timestamp).
312    pub date: u64,
313    /// End-of-day position.
314    pub eod_position: i64,
315    /// Peak intraday position.
316    pub peak_intraday_position: i64,
317    /// Intraday turnover.
318    pub intraday_turnover: i64,
319}
320
321/// Settlement velocity metrics.
322#[derive(Debug, Clone)]
323pub struct SettlementVelocity {
324    /// Instructions settled per second.
325    pub instructions_per_second: f64,
326    /// Value settled per second.
327    pub value_per_second: f64,
328    /// Securities settled per second.
329    pub securities_per_second: f64,
330    /// Peak settlement rate.
331    pub peak_rate: f64,
332}
333
334/// Party efficiency score.
335#[derive(Debug, Clone)]
336pub struct PartyEfficiencyScore {
337    /// Party ID.
338    pub party_id: String,
339    /// Total instructions.
340    pub total_instructions: u64,
341    /// Settled count.
342    pub settled: u64,
343    /// Failed count.
344    pub failed: u64,
345    /// Pending count.
346    pub pending: u64,
347    /// Settlement rate.
348    pub settlement_rate: f64,
349    /// Efficiency score (0-100).
350    pub efficiency_score: f64,
351}
352
353/// Liquidity metrics.
354#[derive(Debug, Clone)]
355pub struct LiquidityMetrics {
356    /// Total value processed.
357    pub total_value_processed: u64,
358    /// Value actually settled.
359    pub settled_value: u64,
360    /// Available liquidity.
361    pub available_liquidity: i64,
362    /// Utilization rate (0-1).
363    pub utilization_rate: f64,
364    /// Turnover ratio.
365    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); // 1 hour
474
475        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        // PARTY_B should have higher score (100% settled vs 67%)
492        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}