1use crate::types::{DVPInstruction, DVPMatchDetail, DVPMatchResult, DVPStatus};
9use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
10use std::collections::HashMap;
11
12#[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 #[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 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 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 (_key, group) in by_security {
66 let deliverers: Vec<_> = group
67 .iter()
68 .filter(|i| i.quantity < 0) .collect();
70 let receivers: Vec<_> = group
71 .iter()
72 .filter(|i| i.quantity > 0) .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 for (j, receiver) in receivers.iter().enumerate() {
111 if !used_receivers[j] {
112 unmatched.push(receiver.id);
113 }
114 }
115 }
116
117 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 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 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 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 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 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 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 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 let quantity = instructions[d_idx].quantity.unsigned_abs() as i64;
240 let payment = instructions[d_idx].payment_amount.unsigned_abs() as i64;
241
242 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#[derive(Debug, Clone)]
269pub struct DVPConfig {
270 pub min_confidence: f64,
272 pub quantity_tolerance: f64,
274 pub amount_tolerance: f64,
276 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#[derive(Debug, Clone)]
293pub struct SettlementSummary {
294 pub pairs_settled: u64,
296 pub instructions_settled: u64,
298 pub total_securities: i64,
300 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, 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, 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; 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; let instructions = vec![delivery, receive];
391 let config = DVPConfig::default();
392
393 let result = DVPMatching::match_instructions(&instructions, &config);
394
395 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; let instructions = vec![delivery, receive];
406 let config = DVPConfig::default();
407
408 let result = DVPMatching::match_instructions(&instructions, &config);
409
410 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}