rustkernel_clearing/
settlement.rs

1//! Settlement execution kernel.
2//!
3//! This module provides settlement execution for clearing:
4//! - Execute settlement instructions
5//! - Track settlement status
6//! - Handle partial settlements
7
8use crate::types::{
9    InstructionType, SettlementExecutionResult, SettlementInstruction, SettlementStatus,
10};
11use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
12use std::collections::HashMap;
13
14// ============================================================================
15// Settlement Execution Kernel
16// ============================================================================
17
18/// Settlement execution kernel.
19///
20/// Executes settlement instructions and tracks their status.
21#[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    /// Create a new settlement execution kernel.
34    #[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    /// Execute settlement instructions.
45    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            // Skip already processed
58            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            // Check eligibility
70            let eligibility = Self::check_eligibility(instruction, context, config);
71
72            match eligibility {
73                EligibilityResult::Eligible => {
74                    // Execute settlement
75                    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                        // Try partial settlement
101                        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    /// Check instruction eligibility.
142    fn check_eligibility(
143        instruction: &SettlementInstruction,
144        context: &SettlementContext,
145        _config: &SettlementConfig,
146    ) -> EligibilityResult {
147        // Check party eligibility
148        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        // Check if on hold
156        if context.parties_on_hold.contains(&instruction.party_id) {
157            return EligibilityResult::Hold(format!("Party {} is on hold", instruction.party_id));
158        }
159
160        // Check balances for deliveries/payments
161        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                // No balance check needed for receives
192            }
193        }
194
195        EligibilityResult::Eligible
196    }
197
198    /// Execute a single instruction.
199    fn execute_instruction(
200        instruction: &SettlementInstruction,
201        _context: &SettlementContext,
202    ) -> Result<(), String> {
203        // In a real implementation, this would:
204        // 1. Update security/cash balances
205        // 2. Record the transaction
206        // 3. Notify counterparties
207
208        // For now, just validate and "execute"
209        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    /// Get settlement statistics by party.
226    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    /// Prioritize instructions for settlement.
258    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/// Settlement context.
291#[derive(Debug, Clone, Default)]
292pub struct SettlementContext {
293    /// Eligible parties.
294    pub eligible_parties: std::collections::HashSet<String>,
295    /// Parties on hold.
296    pub parties_on_hold: std::collections::HashSet<String>,
297    /// Security balances: (party, security) -> quantity.
298    pub security_balances: HashMap<(String, String), i64>,
299    /// Cash balances: party -> amount.
300    pub cash_balances: HashMap<String, i64>,
301}
302
303/// Settlement configuration.
304#[derive(Debug, Clone)]
305pub struct SettlementConfig {
306    /// Fail instruction on error.
307    pub fail_on_error: bool,
308    /// Fail instruction if party is on hold.
309    pub fail_on_hold: bool,
310    /// Allow partial settlements.
311    pub allow_partial: bool,
312    /// Settlement window (seconds from settlement date).
313    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, // 24 hours
323        }
324    }
325}
326
327/// Eligibility check result.
328enum EligibilityResult {
329    Eligible,
330    InsufficientBalance(String),
331    Hold(String),
332    Ineligible(String),
333}
334
335/// Party settlement statistics.
336#[derive(Debug, Clone, Default)]
337pub struct PartySettlementStats {
338    /// Total instructions.
339    pub total_instructions: u64,
340    /// Settled count.
341    pub settled: u64,
342    /// Failed count.
343    pub failed: u64,
344    /// Pending count.
345    pub pending: u64,
346    /// Partial count.
347    pub partial: u64,
348    /// On hold count.
349    pub on_hold: u64,
350    /// Securities volume.
351    pub securities_volume: i64,
352    /// Cash volume.
353    pub cash_volume: i64,
354}
355
356/// Settlement priority.
357#[derive(Debug, Clone, Copy)]
358pub enum SettlementPriority {
359    /// Highest value first.
360    ValueDescending,
361    /// Lowest value first.
362    ValueAscending,
363    /// Earliest date first.
364    DateFirst,
365    /// First in, first out.
366    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; // More than balance
431
432        let context = create_context();
433        let config = SettlementConfig::default();
434
435        let result = SettlementExecution::execute(&mut instructions, &context, &config);
436
437        // With allow_partial, should be pending
438        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        // Party on hold -> pending (unless fail_on_hold)
467        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); // Highest value first
517    }
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}