1use crate::types::{
9 AssetType, CollateralAllocation, CollateralAsset, CollateralOptimizationResult,
10 CollateralRequirement,
11};
12use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
23pub struct CollateralOptimization {
24 metadata: KernelMetadata,
25}
26
27impl Default for CollateralOptimization {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl CollateralOptimization {
34 #[must_use]
36 pub fn new() -> Self {
37 Self {
38 metadata: KernelMetadata::batch("treasury/collateral-opt", Domain::TreasuryManagement)
39 .with_description("Collateral allocation optimization")
40 .with_throughput(5_000)
41 .with_latency_us(1000.0),
42 }
43 }
44
45 pub fn optimize(
47 assets: &[CollateralAsset],
48 requirements: &[CollateralRequirement],
49 config: &CollateralConfig,
50 ) -> CollateralOptimizationResult {
51 let mut sorted_reqs: Vec<_> = requirements.iter().collect();
53 sorted_reqs.sort_by_key(|r| std::cmp::Reverse(r.priority));
54
55 let mut available: HashMap<String, f64> = assets
57 .iter()
58 .filter(|a| !a.is_pledged || config.allow_rehypothecation)
59 .map(|a| (a.id.clone(), a.eligible_value))
60 .collect();
61
62 let mut allocations = Vec::new();
63 let mut total_allocated = 0.0;
64 let mut shortfall = 0.0;
65
66 for req in sorted_reqs {
67 let mut remaining = req.required_amount;
68
69 let mut eligible_assets: Vec<_> = assets
71 .iter()
72 .filter(|a| {
73 req.eligible_types.contains(&a.asset_type)
74 && a.currency == req.currency
75 && available.get(&a.id).copied().unwrap_or(0.0) > 0.0
76 })
77 .collect();
78
79 match config.strategy {
81 OptimizationStrategy::CheapestToDeliver => {
82 eligible_assets.sort_by(|a, b| {
84 b.haircut
85 .partial_cmp(&a.haircut)
86 .unwrap_or(std::cmp::Ordering::Equal)
87 });
88 }
89 OptimizationStrategy::HighestQuality => {
90 eligible_assets.sort_by(|a, b| {
92 a.haircut
93 .partial_cmp(&b.haircut)
94 .unwrap_or(std::cmp::Ordering::Equal)
95 });
96 }
97 OptimizationStrategy::LargestFirst => {
98 eligible_assets.sort_by(|a, b| {
100 b.eligible_value
101 .partial_cmp(&a.eligible_value)
102 .unwrap_or(std::cmp::Ordering::Equal)
103 });
104 }
105 }
106
107 for asset in eligible_assets {
109 if remaining <= 0.0 {
110 break;
111 }
112
113 let available_amount = available.get(&asset.id).copied().unwrap_or(0.0);
114 if available_amount <= 0.0 {
115 continue;
116 }
117
118 let allocate_value = available_amount.min(remaining);
119 let allocate_quantity =
120 allocate_value / (1.0 - asset.haircut) / (asset.market_value / asset.quantity);
121
122 allocations.push(CollateralAllocation {
123 asset_id: asset.id.clone(),
124 counterparty_id: req.counterparty_id.clone(),
125 quantity: allocate_quantity,
126 value: allocate_value,
127 });
128
129 *available.get_mut(&asset.id).unwrap() -= allocate_value;
130 remaining -= allocate_value;
131 total_allocated += allocate_value;
132 }
133
134 if remaining > 0.0 {
135 shortfall += remaining;
136 }
137 }
138
139 let total_required: f64 = requirements.iter().map(|r| r.required_amount).sum();
141 let excess = if total_allocated > total_required {
142 total_allocated - total_required
143 } else {
144 0.0
145 };
146
147 let score = Self::calculate_score(&allocations, assets, requirements, config);
149
150 CollateralOptimizationResult {
151 allocations,
152 total_allocated,
153 excess,
154 shortfall,
155 score,
156 }
157 }
158
159 fn calculate_score(
161 allocations: &[CollateralAllocation],
162 assets: &[CollateralAsset],
163 requirements: &[CollateralRequirement],
164 config: &CollateralConfig,
165 ) -> f64 {
166 if requirements.is_empty() {
167 return 1.0;
168 }
169
170 let total_required: f64 = requirements.iter().map(|r| r.required_amount).sum();
171 if total_required == 0.0 {
172 return 1.0;
173 }
174
175 let total_allocated: f64 = allocations.iter().map(|a| a.value).sum();
177 let coverage = (total_allocated / total_required).min(1.0);
178
179 let quality_factor = if config.strategy == OptimizationStrategy::CheapestToDeliver {
181 let avg_haircut: f64 = allocations
182 .iter()
183 .filter_map(|alloc| {
184 assets
185 .iter()
186 .find(|a| a.id == alloc.asset_id)
187 .map(|a| a.haircut)
188 })
189 .sum::<f64>()
190 / allocations.len().max(1) as f64;
191 avg_haircut } else {
193 1.0 - allocations
194 .iter()
195 .filter_map(|alloc| {
196 assets
197 .iter()
198 .find(|a| a.id == alloc.asset_id)
199 .map(|a| a.haircut)
200 })
201 .sum::<f64>()
202 / allocations.len().max(1) as f64
203 };
204
205 let mut by_counterparty: HashMap<String, f64> = HashMap::new();
207 for alloc in allocations {
208 *by_counterparty
209 .entry(alloc.counterparty_id.clone())
210 .or_default() += alloc.value;
211 }
212 let concentration = if by_counterparty.len() > 1 {
213 1.0 - Self::herfindahl_index(&by_counterparty.values().copied().collect::<Vec<_>>())
214 } else {
215 0.5
216 };
217
218 coverage * 0.6 + quality_factor * 0.25 + concentration * 0.15
220 }
221
222 fn herfindahl_index(values: &[f64]) -> f64 {
224 let total: f64 = values.iter().sum();
225 if total == 0.0 {
226 return 0.0;
227 }
228 values.iter().map(|v| (v / total).powi(2)).sum()
229 }
230
231 pub fn eligible_by_type(assets: &[CollateralAsset]) -> HashMap<AssetType, f64> {
233 let mut by_type: HashMap<AssetType, f64> = HashMap::new();
234 for asset in assets {
235 if !asset.is_pledged {
236 *by_type.entry(asset.asset_type).or_default() += asset.eligible_value;
237 }
238 }
239 by_type
240 }
241
242 pub fn utilization_metrics(
244 assets: &[CollateralAsset],
245 allocations: &[CollateralAllocation],
246 ) -> UtilizationMetrics {
247 let total_available: f64 = assets.iter().map(|a| a.eligible_value).sum();
248 let total_allocated: f64 = allocations.iter().map(|a| a.value).sum();
249
250 let pledged_count = assets.iter().filter(|a| a.is_pledged).count();
251 let pledged_value: f64 = assets
252 .iter()
253 .filter(|a| a.is_pledged)
254 .map(|a| a.market_value)
255 .sum();
256
257 UtilizationMetrics {
258 total_available,
259 total_allocated,
260 utilization_rate: if total_available > 0.0 {
261 total_allocated / total_available
262 } else {
263 0.0
264 },
265 pledged_count: pledged_count as u64,
266 pledged_value,
267 free_collateral: total_available - total_allocated,
268 }
269 }
270}
271
272impl GpuKernel for CollateralOptimization {
273 fn metadata(&self) -> &KernelMetadata {
274 &self.metadata
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct CollateralConfig {
281 pub strategy: OptimizationStrategy,
283 pub allow_rehypothecation: bool,
285 pub min_allocation: f64,
287 pub max_concentration: f64,
289}
290
291impl Default for CollateralConfig {
292 fn default() -> Self {
293 Self {
294 strategy: OptimizationStrategy::CheapestToDeliver,
295 allow_rehypothecation: false,
296 min_allocation: 0.0,
297 max_concentration: 1.0,
298 }
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
304pub enum OptimizationStrategy {
305 CheapestToDeliver,
307 HighestQuality,
309 LargestFirst,
311}
312
313#[derive(Debug, Clone)]
315pub struct UtilizationMetrics {
316 pub total_available: f64,
318 pub total_allocated: f64,
320 pub utilization_rate: f64,
322 pub pledged_count: u64,
324 pub pledged_value: f64,
326 pub free_collateral: f64,
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn create_test_assets() -> Vec<CollateralAsset> {
335 vec![
336 CollateralAsset {
337 id: "CASH_001".to_string(),
338 asset_type: AssetType::Cash,
339 quantity: 1_000_000.0,
340 market_value: 1_000_000.0,
341 haircut: 0.0,
342 eligible_value: 1_000_000.0,
343 currency: "USD".to_string(),
344 is_pledged: false,
345 pledged_to: None,
346 },
347 CollateralAsset {
348 id: "GOV_001".to_string(),
349 asset_type: AssetType::GovBond,
350 quantity: 100.0,
351 market_value: 500_000.0,
352 haircut: 0.02,
353 eligible_value: 490_000.0,
354 currency: "USD".to_string(),
355 is_pledged: false,
356 pledged_to: None,
357 },
358 CollateralAsset {
359 id: "CORP_001".to_string(),
360 asset_type: AssetType::CorpBond,
361 quantity: 200.0,
362 market_value: 400_000.0,
363 haircut: 0.10,
364 eligible_value: 360_000.0,
365 currency: "USD".to_string(),
366 is_pledged: false,
367 pledged_to: None,
368 },
369 ]
370 }
371
372 fn create_test_requirements() -> Vec<CollateralRequirement> {
373 vec![
374 CollateralRequirement {
375 counterparty_id: "CP_A".to_string(),
376 required_amount: 300_000.0,
377 currency: "USD".to_string(),
378 eligible_types: vec![AssetType::Cash, AssetType::GovBond],
379 priority: 1,
380 },
381 CollateralRequirement {
382 counterparty_id: "CP_B".to_string(),
383 required_amount: 200_000.0,
384 currency: "USD".to_string(),
385 eligible_types: vec![AssetType::Cash, AssetType::GovBond, AssetType::CorpBond],
386 priority: 2,
387 },
388 ]
389 }
390
391 #[test]
392 fn test_collateral_metadata() {
393 let kernel = CollateralOptimization::new();
394 assert_eq!(kernel.metadata().id, "treasury/collateral-opt");
395 assert_eq!(kernel.metadata().domain, Domain::TreasuryManagement);
396 }
397
398 #[test]
399 fn test_basic_optimization() {
400 let assets = create_test_assets();
401 let requirements = create_test_requirements();
402 let config = CollateralConfig::default();
403
404 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
405
406 assert!(result.shortfall < 0.01);
407 assert!(result.total_allocated >= 500_000.0);
408 assert!(!result.allocations.is_empty());
409 }
410
411 #[test]
412 fn test_cheapest_to_deliver() {
413 let assets = create_test_assets();
414 let requirements = vec![CollateralRequirement {
415 counterparty_id: "CP_A".to_string(),
416 required_amount: 300_000.0,
417 currency: "USD".to_string(),
418 eligible_types: vec![AssetType::Cash, AssetType::GovBond, AssetType::CorpBond],
419 priority: 1,
420 }];
421
422 let config = CollateralConfig {
423 strategy: OptimizationStrategy::CheapestToDeliver,
424 ..Default::default()
425 };
426
427 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
428
429 let first_alloc = &result.allocations[0];
431 assert_eq!(first_alloc.asset_id, "CORP_001");
432 }
433
434 #[test]
435 fn test_highest_quality() {
436 let assets = create_test_assets();
437 let requirements = vec![CollateralRequirement {
438 counterparty_id: "CP_A".to_string(),
439 required_amount: 300_000.0,
440 currency: "USD".to_string(),
441 eligible_types: vec![AssetType::Cash, AssetType::GovBond, AssetType::CorpBond],
442 priority: 1,
443 }];
444
445 let config = CollateralConfig {
446 strategy: OptimizationStrategy::HighestQuality,
447 ..Default::default()
448 };
449
450 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
451
452 let first_alloc = &result.allocations[0];
454 assert_eq!(first_alloc.asset_id, "CASH_001");
455 }
456
457 #[test]
458 fn test_shortfall() {
459 let assets = create_test_assets();
460 let requirements = vec![CollateralRequirement {
461 counterparty_id: "CP_A".to_string(),
462 required_amount: 5_000_000.0, currency: "USD".to_string(),
464 eligible_types: vec![AssetType::Cash, AssetType::GovBond, AssetType::CorpBond],
465 priority: 1,
466 }];
467
468 let config = CollateralConfig::default();
469 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
470
471 assert!(result.shortfall > 0.0);
472 }
473
474 #[test]
475 fn test_currency_filtering() {
476 let assets = create_test_assets();
477 let requirements = vec![CollateralRequirement {
478 counterparty_id: "CP_A".to_string(),
479 required_amount: 300_000.0,
480 currency: "EUR".to_string(), eligible_types: vec![AssetType::Cash, AssetType::GovBond],
482 priority: 1,
483 }];
484
485 let config = CollateralConfig::default();
486 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
487
488 assert!((result.shortfall - 300_000.0).abs() < 0.01);
490 }
491
492 #[test]
493 fn test_pledged_assets() {
494 let mut assets = create_test_assets();
495 assets[0].is_pledged = true; let requirements = vec![CollateralRequirement {
498 counterparty_id: "CP_A".to_string(),
499 required_amount: 1_500_000.0,
500 currency: "USD".to_string(),
501 eligible_types: vec![AssetType::Cash, AssetType::GovBond, AssetType::CorpBond],
502 priority: 1,
503 }];
504
505 let config = CollateralConfig {
506 allow_rehypothecation: false,
507 ..Default::default()
508 };
509
510 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
511
512 assert!(!result.allocations.iter().any(|a| a.asset_id == "CASH_001"));
514 }
515
516 #[test]
517 fn test_eligible_by_type() {
518 let assets = create_test_assets();
519 let by_type = CollateralOptimization::eligible_by_type(&assets);
520
521 assert_eq!(by_type.get(&AssetType::Cash), Some(&1_000_000.0));
522 assert_eq!(by_type.get(&AssetType::GovBond), Some(&490_000.0));
523 assert_eq!(by_type.get(&AssetType::CorpBond), Some(&360_000.0));
524 }
525
526 #[test]
527 fn test_utilization_metrics() {
528 let assets = create_test_assets();
529 let allocations = vec![CollateralAllocation {
530 asset_id: "CASH_001".to_string(),
531 counterparty_id: "CP_A".to_string(),
532 quantity: 500_000.0,
533 value: 500_000.0,
534 }];
535
536 let metrics = CollateralOptimization::utilization_metrics(&assets, &allocations);
537
538 assert_eq!(metrics.total_available, 1_850_000.0);
539 assert_eq!(metrics.total_allocated, 500_000.0);
540 assert!((metrics.utilization_rate - 500_000.0 / 1_850_000.0).abs() < 0.001);
541 }
542
543 #[test]
544 fn test_priority_ordering() {
545 let assets = vec![CollateralAsset {
546 id: "CASH_001".to_string(),
547 asset_type: AssetType::Cash,
548 quantity: 500_000.0,
549 market_value: 500_000.0,
550 haircut: 0.0,
551 eligible_value: 500_000.0,
552 currency: "USD".to_string(),
553 is_pledged: false,
554 pledged_to: None,
555 }];
556
557 let requirements = vec![
558 CollateralRequirement {
559 counterparty_id: "LOW".to_string(),
560 required_amount: 300_000.0,
561 currency: "USD".to_string(),
562 eligible_types: vec![AssetType::Cash],
563 priority: 1, },
565 CollateralRequirement {
566 counterparty_id: "HIGH".to_string(),
567 required_amount: 300_000.0,
568 currency: "USD".to_string(),
569 eligible_types: vec![AssetType::Cash],
570 priority: 10, },
572 ];
573
574 let config = CollateralConfig::default();
575 let result = CollateralOptimization::optimize(&assets, &requirements, &config);
576
577 let high_alloc: f64 = result
579 .allocations
580 .iter()
581 .filter(|a| a.counterparty_id == "HIGH")
582 .map(|a| a.value)
583 .sum();
584 assert!((high_alloc - 300_000.0).abs() < 0.01);
585 }
586}