1use crate::types::{
9 InstructionType, SettlementExecutionResult, SettlementInstruction, SettlementStatus,
10};
11use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
22pub struct SettlementExecution {
23 metadata: KernelMetadata,
24}
25
26impl Default for SettlementExecution {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl SettlementExecution {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 metadata: KernelMetadata::ring("clearing/settlement", Domain::Clearing)
38 .with_description("Settlement execution and tracking")
39 .with_throughput(20_000)
40 .with_latency_us(200.0),
41 }
42 }
43
44 pub fn execute(
46 instructions: &mut [SettlementInstruction],
47 context: &SettlementContext,
48 config: &SettlementConfig,
49 ) -> SettlementExecutionResult {
50 let mut settled = Vec::new();
51 let mut failed = Vec::new();
52 let mut pending = Vec::new();
53 let mut value_settled = 0i64;
54 let mut value_failed = 0i64;
55
56 for instruction in instructions.iter_mut() {
57 if matches!(
59 instruction.status,
60 SettlementStatus::Settled | SettlementStatus::Failed
61 ) {
62 if instruction.status == SettlementStatus::Settled {
63 settled.push(instruction.id);
64 value_settled += instruction.payment_amount.unsigned_abs() as i64;
65 }
66 continue;
67 }
68
69 let eligibility = Self::check_eligibility(instruction, context, config);
71
72 match eligibility {
73 EligibilityResult::Eligible => {
74 match Self::execute_instruction(instruction, context) {
76 Ok(()) => {
77 instruction.status = SettlementStatus::Settled;
78 settled.push(instruction.id);
79 value_settled += instruction.payment_amount.unsigned_abs() as i64;
80 }
81 Err(reason) => {
82 if config.fail_on_error {
83 instruction.status = SettlementStatus::Failed;
84 failed.push((instruction.id, reason));
85 value_failed += instruction.payment_amount.unsigned_abs() as i64;
86 } else {
87 instruction.status = SettlementStatus::Pending;
88 pending.push(instruction.id);
89 }
90 }
91 }
92 }
93 EligibilityResult::InsufficientBalance(reason) => {
94 if config.allow_partial
95 && matches!(
96 instruction.instruction_type,
97 InstructionType::Deliver | InstructionType::Pay
98 )
99 {
100 instruction.status = SettlementStatus::Partial;
102 pending.push(instruction.id);
103 } else {
104 instruction.status = SettlementStatus::Failed;
105 failed.push((instruction.id, reason));
106 value_failed += instruction.payment_amount.unsigned_abs() as i64;
107 }
108 }
109 EligibilityResult::Hold(reason) => {
110 instruction.status = SettlementStatus::OnHold;
111 pending.push(instruction.id);
112 if config.fail_on_hold {
113 failed.push((instruction.id, reason));
114 }
115 }
116 EligibilityResult::Ineligible(reason) => {
117 instruction.status = SettlementStatus::Failed;
118 failed.push((instruction.id, reason));
119 value_failed += instruction.payment_amount.unsigned_abs() as i64;
120 }
121 }
122 }
123
124 let total = settled.len() + failed.len() + pending.len();
125 let settlement_rate = if total > 0 {
126 settled.len() as f64 / total as f64
127 } else {
128 0.0
129 };
130
131 SettlementExecutionResult {
132 settled,
133 failed,
134 pending,
135 settlement_rate,
136 value_settled,
137 value_failed,
138 }
139 }
140
141 fn check_eligibility(
143 instruction: &SettlementInstruction,
144 context: &SettlementContext,
145 _config: &SettlementConfig,
146 ) -> EligibilityResult {
147 if !context.eligible_parties.contains(&instruction.party_id) {
149 return EligibilityResult::Ineligible(format!(
150 "Party {} not eligible for settlement",
151 instruction.party_id
152 ));
153 }
154
155 if context.parties_on_hold.contains(&instruction.party_id) {
157 return EligibilityResult::Hold(format!("Party {} is on hold", instruction.party_id));
158 }
159
160 match instruction.instruction_type {
162 InstructionType::Deliver => {
163 let key = (
164 instruction.party_id.clone(),
165 instruction.security_id.clone(),
166 );
167 let balance = context.security_balances.get(&key).copied().unwrap_or(0);
168 if balance < instruction.quantity.unsigned_abs() as i64 {
169 return EligibilityResult::InsufficientBalance(format!(
170 "Insufficient securities: need {}, have {}",
171 instruction.quantity.unsigned_abs(),
172 balance
173 ));
174 }
175 }
176 InstructionType::Pay => {
177 let balance = context
178 .cash_balances
179 .get(&instruction.party_id)
180 .copied()
181 .unwrap_or(0);
182 let required = instruction.payment_amount.unsigned_abs() as i64;
183 if balance < required {
184 return EligibilityResult::InsufficientBalance(format!(
185 "Insufficient cash: need {}, have {}",
186 required, balance
187 ));
188 }
189 }
190 InstructionType::Receive | InstructionType::Collect => {
191 }
193 }
194
195 EligibilityResult::Eligible
196 }
197
198 fn execute_instruction(
200 instruction: &SettlementInstruction,
201 _context: &SettlementContext,
202 ) -> Result<(), String> {
203 match instruction.instruction_type {
210 InstructionType::Deliver | InstructionType::Receive => {
211 if instruction.quantity == 0 {
212 return Err("Cannot settle zero quantity".to_string());
213 }
214 }
215 InstructionType::Pay | InstructionType::Collect => {
216 if instruction.payment_amount == 0 {
217 return Err("Cannot settle zero payment".to_string());
218 }
219 }
220 }
221
222 Ok(())
223 }
224
225 pub fn stats_by_party(
227 instructions: &[SettlementInstruction],
228 ) -> HashMap<String, PartySettlementStats> {
229 let mut stats: HashMap<String, PartySettlementStats> = HashMap::new();
230
231 for instr in instructions {
232 let stat = stats.entry(instr.party_id.clone()).or_default();
233
234 stat.total_instructions += 1;
235
236 match instr.status {
237 SettlementStatus::Settled => stat.settled += 1,
238 SettlementStatus::Failed => stat.failed += 1,
239 SettlementStatus::Pending | SettlementStatus::InProgress => stat.pending += 1,
240 SettlementStatus::Partial => stat.partial += 1,
241 SettlementStatus::OnHold => stat.on_hold += 1,
242 }
243
244 match instr.instruction_type {
245 InstructionType::Deliver | InstructionType::Receive => {
246 stat.securities_volume += instr.quantity.unsigned_abs() as i64;
247 }
248 InstructionType::Pay | InstructionType::Collect => {
249 stat.cash_volume += instr.payment_amount.unsigned_abs() as i64;
250 }
251 }
252 }
253
254 stats
255 }
256
257 pub fn prioritize(instructions: &mut [SettlementInstruction], priority: SettlementPriority) {
259 match priority {
260 SettlementPriority::ValueDescending => {
261 instructions.sort_by(|a, b| {
262 b.payment_amount
263 .unsigned_abs()
264 .cmp(&a.payment_amount.unsigned_abs())
265 });
266 }
267 SettlementPriority::ValueAscending => {
268 instructions.sort_by(|a, b| {
269 a.payment_amount
270 .unsigned_abs()
271 .cmp(&b.payment_amount.unsigned_abs())
272 });
273 }
274 SettlementPriority::DateFirst => {
275 instructions.sort_by_key(|i| i.settlement_date);
276 }
277 SettlementPriority::Fifo => {
278 instructions.sort_by_key(|i| i.id);
279 }
280 }
281 }
282}
283
284impl GpuKernel for SettlementExecution {
285 fn metadata(&self) -> &KernelMetadata {
286 &self.metadata
287 }
288}
289
290#[derive(Debug, Clone, Default)]
292pub struct SettlementContext {
293 pub eligible_parties: std::collections::HashSet<String>,
295 pub parties_on_hold: std::collections::HashSet<String>,
297 pub security_balances: HashMap<(String, String), i64>,
299 pub cash_balances: HashMap<String, i64>,
301}
302
303#[derive(Debug, Clone)]
305pub struct SettlementConfig {
306 pub fail_on_error: bool,
308 pub fail_on_hold: bool,
310 pub allow_partial: bool,
312 pub settlement_window_seconds: u64,
314}
315
316impl Default for SettlementConfig {
317 fn default() -> Self {
318 Self {
319 fail_on_error: true,
320 fail_on_hold: false,
321 allow_partial: true,
322 settlement_window_seconds: 86400, }
324 }
325}
326
327enum EligibilityResult {
329 Eligible,
330 InsufficientBalance(String),
331 Hold(String),
332 Ineligible(String),
333}
334
335#[derive(Debug, Clone, Default)]
337pub struct PartySettlementStats {
338 pub total_instructions: u64,
340 pub settled: u64,
342 pub failed: u64,
344 pub pending: u64,
346 pub partial: u64,
348 pub on_hold: u64,
350 pub securities_volume: i64,
352 pub cash_volume: i64,
354}
355
356#[derive(Debug, Clone, Copy)]
358pub enum SettlementPriority {
359 ValueDescending,
361 ValueAscending,
363 DateFirst,
365 Fifo,
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 fn create_context() -> SettlementContext {
374 let mut ctx = SettlementContext::default();
375 ctx.eligible_parties.insert("PARTY_A".to_string());
376 ctx.eligible_parties.insert("PARTY_B".to_string());
377 ctx.security_balances
378 .insert(("PARTY_A".to_string(), "AAPL".to_string()), 1000);
379 ctx.cash_balances.insert("PARTY_A".to_string(), 1_000_000);
380 ctx.cash_balances.insert("PARTY_B".to_string(), 500_000);
381 ctx
382 }
383
384 fn create_instruction(
385 id: u64,
386 party: &str,
387 instr_type: InstructionType,
388 ) -> SettlementInstruction {
389 SettlementInstruction {
390 id,
391 party_id: party.to_string(),
392 security_id: "AAPL".to_string(),
393 instruction_type: instr_type,
394 quantity: 100,
395 payment_amount: 15000,
396 currency: "USD".to_string(),
397 settlement_date: 1700172800,
398 status: SettlementStatus::Pending,
399 source_trades: vec![1],
400 }
401 }
402
403 #[test]
404 fn test_settlement_metadata() {
405 let kernel = SettlementExecution::new();
406 assert_eq!(kernel.metadata().id, "clearing/settlement");
407 assert_eq!(kernel.metadata().domain, Domain::Clearing);
408 }
409
410 #[test]
411 fn test_successful_settlement() {
412 let mut instructions = vec![
413 create_instruction(1, "PARTY_A", InstructionType::Deliver),
414 create_instruction(2, "PARTY_B", InstructionType::Receive),
415 ];
416
417 let context = create_context();
418 let config = SettlementConfig::default();
419
420 let result = SettlementExecution::execute(&mut instructions, &context, &config);
421
422 assert_eq!(result.settled.len(), 2);
423 assert!(result.failed.is_empty());
424 assert!((result.settlement_rate - 1.0).abs() < 0.001);
425 }
426
427 #[test]
428 fn test_insufficient_balance() {
429 let mut instructions = vec![create_instruction(1, "PARTY_A", InstructionType::Deliver)];
430 instructions[0].quantity = 10000; let context = create_context();
433 let config = SettlementConfig::default();
434
435 let result = SettlementExecution::execute(&mut instructions, &context, &config);
436
437 assert!(result.settled.is_empty());
439 assert_eq!(result.pending.len(), 1);
440 }
441
442 #[test]
443 fn test_ineligible_party() {
444 let mut instructions = vec![create_instruction(1, "UNKNOWN", InstructionType::Deliver)];
445
446 let context = create_context();
447 let config = SettlementConfig::default();
448
449 let result = SettlementExecution::execute(&mut instructions, &context, &config);
450
451 assert!(result.settled.is_empty());
452 assert_eq!(result.failed.len(), 1);
453 }
454
455 #[test]
456 fn test_party_on_hold() {
457 let mut instructions = vec![create_instruction(1, "PARTY_A", InstructionType::Deliver)];
458
459 let mut context = create_context();
460 context.parties_on_hold.insert("PARTY_A".to_string());
461
462 let config = SettlementConfig::default();
463
464 let result = SettlementExecution::execute(&mut instructions, &context, &config);
465
466 assert_eq!(result.pending.len(), 1);
468 assert_eq!(instructions[0].status, SettlementStatus::OnHold);
469 }
470
471 #[test]
472 fn test_stats_by_party() {
473 let instructions = vec![
474 {
475 let mut i = create_instruction(1, "PARTY_A", InstructionType::Deliver);
476 i.status = SettlementStatus::Settled;
477 i
478 },
479 {
480 let mut i = create_instruction(2, "PARTY_A", InstructionType::Deliver);
481 i.status = SettlementStatus::Failed;
482 i
483 },
484 {
485 let mut i = create_instruction(3, "PARTY_B", InstructionType::Receive);
486 i.status = SettlementStatus::Settled;
487 i
488 },
489 ];
490
491 let stats = SettlementExecution::stats_by_party(&instructions);
492
493 let a_stats = stats.get("PARTY_A").unwrap();
494 assert_eq!(a_stats.total_instructions, 2);
495 assert_eq!(a_stats.settled, 1);
496 assert_eq!(a_stats.failed, 1);
497
498 let b_stats = stats.get("PARTY_B").unwrap();
499 assert_eq!(b_stats.settled, 1);
500 }
501
502 #[test]
503 fn test_prioritize_value_desc() {
504 let mut instructions = vec![
505 create_instruction(1, "PARTY_A", InstructionType::Pay),
506 {
507 let mut i = create_instruction(2, "PARTY_A", InstructionType::Pay);
508 i.payment_amount = 50000;
509 i
510 },
511 create_instruction(3, "PARTY_A", InstructionType::Pay),
512 ];
513
514 SettlementExecution::prioritize(&mut instructions, SettlementPriority::ValueDescending);
515
516 assert_eq!(instructions[0].id, 2); }
518
519 #[test]
520 fn test_prioritize_fifo() {
521 let mut instructions = vec![
522 create_instruction(3, "PARTY_A", InstructionType::Pay),
523 create_instruction(1, "PARTY_A", InstructionType::Pay),
524 create_instruction(2, "PARTY_A", InstructionType::Pay),
525 ];
526
527 SettlementExecution::prioritize(&mut instructions, SettlementPriority::Fifo);
528
529 assert_eq!(instructions[0].id, 1);
530 assert_eq!(instructions[1].id, 2);
531 assert_eq!(instructions[2].id, 3);
532 }
533
534 #[test]
535 fn test_zero_quantity_rejected() {
536 let mut instructions = vec![{
537 let mut i = create_instruction(1, "PARTY_A", InstructionType::Deliver);
538 i.quantity = 0;
539 i
540 }];
541
542 let context = create_context();
543 let config = SettlementConfig::default();
544
545 let result = SettlementExecution::execute(&mut instructions, &context, &config);
546
547 assert!(result.settled.is_empty());
548 assert_eq!(result.failed.len(), 1);
549 }
550}