rustkernel_clearing/
dvp.rs

1//! DVP (Delivery vs Payment) matching kernel.
2//!
3//! This module provides DVP matching for clearing:
4//! - Match delivery and payment instructions
5//! - Identify discrepancies
6//! - Calculate match confidence
7
8use crate::types::{DVPInstruction, DVPMatchDetail, DVPMatchResult, DVPStatus};
9use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
10use std::collections::HashMap;
11
12// ============================================================================
13// DVP Matching Kernel
14// ============================================================================
15
16/// DVP matching kernel.
17///
18/// Matches delivery instructions with corresponding payment instructions.
19#[derive(Debug, Clone)]
20pub struct DVPMatching {
21    metadata: KernelMetadata,
22}
23
24impl Default for DVPMatching {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl DVPMatching {
31    /// Create a new DVP matching kernel.
32    #[must_use]
33    pub fn new() -> Self {
34        Self {
35            metadata: KernelMetadata::ring("clearing/dvp-matching", Domain::Clearing)
36                .with_description("Delivery vs payment matching")
37                .with_throughput(50_000)
38                .with_latency_us(100.0),
39        }
40    }
41
42    /// Match DVP instructions.
43    pub fn match_instructions(
44        instructions: &[DVPInstruction],
45        config: &DVPConfig,
46    ) -> DVPMatchResult {
47        let mut matched_pairs = Vec::new();
48        let mut unmatched: Vec<u64> = Vec::new();
49        let mut details = Vec::new();
50
51        // Group instructions by security and settlement date
52        let mut by_security: HashMap<String, Vec<&DVPInstruction>> = HashMap::new();
53
54        for instr in instructions {
55            if instr.status != DVPStatus::Pending {
56                continue;
57            }
58            by_security
59                .entry(format!("{}:{}", instr.security_id, instr.settlement_date))
60                .or_default()
61                .push(instr);
62        }
63
64        // For each security/date group, match deliverers with receivers
65        for (_key, group) in by_security {
66            let deliverers: Vec<_> = group
67                .iter()
68                .filter(|i| i.quantity < 0) // Negative = delivering
69                .collect();
70            let receivers: Vec<_> = group
71                .iter()
72                .filter(|i| i.quantity > 0) // Positive = receiving
73                .collect();
74
75            let mut used_receivers: Vec<bool> = vec![false; receivers.len()];
76
77            for deliverer in &deliverers {
78                let mut best_match: Option<(usize, f64, Vec<String>)> = None;
79
80                for (j, receiver) in receivers.iter().enumerate() {
81                    if used_receivers[j] {
82                        continue;
83                    }
84
85                    let (confidence, differences) =
86                        Self::calculate_match_score(deliverer, receiver, config);
87
88                    if confidence >= config.min_confidence
89                        && (best_match.is_none() || confidence > best_match.as_ref().unwrap().1)
90                    {
91                        best_match = Some((j, confidence, differences));
92                    }
93                }
94
95                if let Some((j, confidence, differences)) = best_match {
96                    used_receivers[j] = true;
97                    matched_pairs.push((deliverer.id, receivers[j].id));
98                    details.push(DVPMatchDetail {
99                        delivery_id: deliverer.id,
100                        payment_id: receivers[j].id,
101                        confidence,
102                        differences,
103                    });
104                } else {
105                    unmatched.push(deliverer.id);
106                }
107            }
108
109            // Add unmatched receivers
110            for (j, receiver) in receivers.iter().enumerate() {
111                if !used_receivers[j] {
112                    unmatched.push(receiver.id);
113                }
114            }
115        }
116
117        // Add any instructions that weren't in a group (non-pending)
118        for instr in instructions {
119            if instr.status != DVPStatus::Pending {
120                unmatched.push(instr.id);
121            }
122        }
123
124        let total_pending = instructions
125            .iter()
126            .filter(|i| i.status == DVPStatus::Pending)
127            .count();
128        let matched_count = matched_pairs.len() * 2;
129        let match_rate = if total_pending > 0 {
130            matched_count as f64 / total_pending as f64
131        } else {
132            0.0
133        };
134
135        DVPMatchResult {
136            matched_pairs,
137            unmatched,
138            match_rate,
139            details,
140        }
141    }
142
143    /// Calculate match score between a delivery and receive instruction.
144    fn calculate_match_score(
145        delivery: &DVPInstruction,
146        receive: &DVPInstruction,
147        config: &DVPConfig,
148    ) -> (f64, Vec<String>) {
149        let mut score = 1.0;
150        let mut differences = Vec::new();
151
152        // Check counterparties match
153        if delivery.deliverer != receive.receiver {
154            differences.push(format!(
155                "Deliverer mismatch: {} vs {}",
156                delivery.deliverer, receive.receiver
157            ));
158            if config.strict_counterparty {
159                return (0.0, differences);
160            }
161            score *= 0.5;
162        }
163
164        if delivery.receiver != receive.deliverer {
165            differences.push(format!(
166                "Receiver mismatch: {} vs {}",
167                delivery.receiver, receive.deliverer
168            ));
169            if config.strict_counterparty {
170                return (0.0, differences);
171            }
172            score *= 0.5;
173        }
174
175        // Check quantity matches (absolute value)
176        let delivery_qty = delivery.quantity.unsigned_abs();
177        let receive_qty = receive.quantity.unsigned_abs();
178        if delivery_qty != receive_qty {
179            let qty_diff = (delivery_qty as f64 - receive_qty as f64).abs();
180            let qty_pct = qty_diff / delivery_qty.max(receive_qty) as f64;
181            differences.push(format!(
182                "Quantity mismatch: {} vs {} ({:.2}%)",
183                delivery_qty,
184                receive_qty,
185                qty_pct * 100.0
186            ));
187            if qty_pct > config.quantity_tolerance {
188                return (0.0, differences);
189            }
190            score *= 1.0 - qty_pct;
191        }
192
193        // Check payment amount matches
194        let delivery_amt = delivery.payment_amount.unsigned_abs();
195        let receive_amt = receive.payment_amount.unsigned_abs();
196        if delivery_amt != receive_amt {
197            let amt_diff = (delivery_amt as f64 - receive_amt as f64).abs();
198            let amt_pct = amt_diff / delivery_amt.max(receive_amt) as f64;
199            differences.push(format!(
200                "Payment mismatch: {} vs {} ({:.2}%)",
201                delivery_amt,
202                receive_amt,
203                amt_pct * 100.0
204            ));
205            if amt_pct > config.amount_tolerance {
206                return (0.0, differences);
207            }
208            score *= 1.0 - amt_pct;
209        }
210
211        // Check currency matches
212        if delivery.currency != receive.currency {
213            differences.push(format!(
214                "Currency mismatch: {} vs {}",
215                delivery.currency, receive.currency
216            ));
217            return (0.0, differences);
218        }
219
220        (score, differences)
221    }
222
223    /// Execute settlement for matched pairs.
224    pub fn execute_settlement(
225        instructions: &mut [DVPInstruction],
226        matched_pairs: &[(u64, u64)],
227    ) -> SettlementSummary {
228        let mut securities_settled = 0i64;
229        let mut payments_settled = 0i64;
230        let mut settled_count = 0u64;
231
232        for (delivery_id, receive_id) in matched_pairs {
233            // Find indices first
234            let delivery_idx = instructions.iter().position(|i| i.id == *delivery_id);
235            let receive_idx = instructions.iter().position(|i| i.id == *receive_id);
236
237            if let (Some(d_idx), Some(r_idx)) = (delivery_idx, receive_idx) {
238                // Capture values before mutating
239                let quantity = instructions[d_idx].quantity.unsigned_abs() as i64;
240                let payment = instructions[d_idx].payment_amount.unsigned_abs() as i64;
241
242                // Update statuses
243                instructions[d_idx].status = DVPStatus::Settled;
244                instructions[r_idx].status = DVPStatus::Settled;
245
246                securities_settled += quantity;
247                payments_settled += payment;
248                settled_count += 2;
249            }
250        }
251
252        SettlementSummary {
253            pairs_settled: matched_pairs.len() as u64,
254            instructions_settled: settled_count,
255            total_securities: securities_settled,
256            total_payments: payments_settled,
257        }
258    }
259}
260
261impl GpuKernel for DVPMatching {
262    fn metadata(&self) -> &KernelMetadata {
263        &self.metadata
264    }
265}
266
267/// DVP matching configuration.
268#[derive(Debug, Clone)]
269pub struct DVPConfig {
270    /// Minimum confidence for a match.
271    pub min_confidence: f64,
272    /// Quantity tolerance (fraction).
273    pub quantity_tolerance: f64,
274    /// Amount tolerance (fraction).
275    pub amount_tolerance: f64,
276    /// Strict counterparty matching.
277    pub strict_counterparty: bool,
278}
279
280impl Default for DVPConfig {
281    fn default() -> Self {
282        Self {
283            min_confidence: 0.8,
284            quantity_tolerance: 0.01,
285            amount_tolerance: 0.01,
286            strict_counterparty: true,
287        }
288    }
289}
290
291/// Settlement summary.
292#[derive(Debug, Clone)]
293pub struct SettlementSummary {
294    /// Number of pairs settled.
295    pub pairs_settled: u64,
296    /// Total instructions settled.
297    pub instructions_settled: u64,
298    /// Total securities settled.
299    pub total_securities: i64,
300    /// Total payments settled.
301    pub total_payments: i64,
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    fn create_matching_pair() -> (DVPInstruction, DVPInstruction) {
309        let delivery = DVPInstruction {
310            id: 1,
311            trade_id: 100,
312            security_id: "AAPL".to_string(),
313            deliverer: "PARTY_A".to_string(),
314            receiver: "PARTY_B".to_string(),
315            quantity: -100, // Delivering
316            payment_amount: -15000,
317            currency: "USD".to_string(),
318            settlement_date: 1700172800,
319            status: DVPStatus::Pending,
320        };
321
322        let receive = DVPInstruction {
323            id: 2,
324            trade_id: 100,
325            security_id: "AAPL".to_string(),
326            deliverer: "PARTY_B".to_string(),
327            receiver: "PARTY_A".to_string(),
328            quantity: 100, // Receiving
329            payment_amount: 15000,
330            currency: "USD".to_string(),
331            settlement_date: 1700172800,
332            status: DVPStatus::Pending,
333        };
334
335        (delivery, receive)
336    }
337
338    #[test]
339    fn test_dvp_metadata() {
340        let kernel = DVPMatching::new();
341        assert_eq!(kernel.metadata().id, "clearing/dvp-matching");
342        assert_eq!(kernel.metadata().domain, Domain::Clearing);
343    }
344
345    #[test]
346    fn test_perfect_match() {
347        let (delivery, receive) = create_matching_pair();
348        let instructions = vec![delivery, receive];
349        let config = DVPConfig::default();
350
351        let result = DVPMatching::match_instructions(&instructions, &config);
352
353        assert_eq!(result.matched_pairs.len(), 1);
354        assert!(result.unmatched.is_empty());
355        assert!((result.match_rate - 1.0).abs() < 0.001);
356    }
357
358    #[test]
359    fn test_no_match_different_security() {
360        let (mut delivery, receive) = create_matching_pair();
361        delivery.security_id = "MSFT".to_string();
362
363        let instructions = vec![delivery, receive];
364        let config = DVPConfig::default();
365
366        let result = DVPMatching::match_instructions(&instructions, &config);
367
368        assert!(result.matched_pairs.is_empty());
369        assert_eq!(result.unmatched.len(), 2);
370    }
371
372    #[test]
373    fn test_no_match_different_settlement_date() {
374        let (mut delivery, receive) = create_matching_pair();
375        delivery.settlement_date = 1700259200; // Different date
376
377        let instructions = vec![delivery, receive];
378        let config = DVPConfig::default();
379
380        let result = DVPMatching::match_instructions(&instructions, &config);
381
382        assert!(result.matched_pairs.is_empty());
383    }
384
385    #[test]
386    fn test_quantity_mismatch() {
387        let (mut delivery, receive) = create_matching_pair();
388        delivery.quantity = -99; // Slight mismatch
389
390        let instructions = vec![delivery, receive];
391        let config = DVPConfig::default();
392
393        let result = DVPMatching::match_instructions(&instructions, &config);
394
395        // Should still match due to tolerance
396        assert_eq!(result.matched_pairs.len(), 1);
397        assert!(result.details[0].confidence < 1.0);
398    }
399
400    #[test]
401    fn test_quantity_mismatch_too_large() {
402        let (mut delivery, receive) = create_matching_pair();
403        delivery.quantity = -50; // 50% mismatch
404
405        let instructions = vec![delivery, receive];
406        let config = DVPConfig::default();
407
408        let result = DVPMatching::match_instructions(&instructions, &config);
409
410        // Should not match due to large difference
411        assert!(result.matched_pairs.is_empty());
412    }
413
414    #[test]
415    fn test_currency_mismatch() {
416        let (mut delivery, receive) = create_matching_pair();
417        delivery.currency = "EUR".to_string();
418
419        let instructions = vec![delivery, receive];
420        let config = DVPConfig::default();
421
422        let result = DVPMatching::match_instructions(&instructions, &config);
423
424        assert!(result.matched_pairs.is_empty());
425    }
426
427    #[test]
428    fn test_multiple_pairs() {
429        let (d1, r1) = create_matching_pair();
430        let (mut d2, mut r2) = create_matching_pair();
431        d2.id = 3;
432        d2.trade_id = 101;
433        r2.id = 4;
434        r2.trade_id = 101;
435
436        let instructions = vec![d1, r1, d2, r2];
437        let config = DVPConfig::default();
438
439        let result = DVPMatching::match_instructions(&instructions, &config);
440
441        assert_eq!(result.matched_pairs.len(), 2);
442        assert!(result.unmatched.is_empty());
443    }
444
445    #[test]
446    fn test_skip_non_pending() {
447        let (mut delivery, receive) = create_matching_pair();
448        delivery.status = DVPStatus::Matched;
449
450        let instructions = vec![delivery, receive];
451        let config = DVPConfig::default();
452
453        let result = DVPMatching::match_instructions(&instructions, &config);
454
455        assert!(result.matched_pairs.is_empty());
456        assert_eq!(result.unmatched.len(), 2);
457    }
458
459    #[test]
460    fn test_execute_settlement() {
461        let (delivery, receive) = create_matching_pair();
462        let mut instructions = vec![delivery, receive];
463        let matched_pairs = vec![(1, 2)];
464
465        let summary = DVPMatching::execute_settlement(&mut instructions, &matched_pairs);
466
467        assert_eq!(summary.pairs_settled, 1);
468        assert_eq!(summary.instructions_settled, 2);
469        assert_eq!(instructions[0].status, DVPStatus::Settled);
470        assert_eq!(instructions[1].status, DVPStatus::Settled);
471    }
472
473    #[test]
474    fn test_match_confidence() {
475        let (delivery, receive) = create_matching_pair();
476        let config = DVPConfig::default();
477
478        let (confidence, differences) =
479            DVPMatching::calculate_match_score(&delivery, &receive, &config);
480
481        assert!((confidence - 1.0).abs() < 0.001);
482        assert!(differences.is_empty());
483    }
484}