Skip to content

Commit e9c4eed

Browse files
mivertowskiclaude
andcommitted
Phase 7: Complete stub implementations and algorithm upgrades
Phase 7.1-7.2 (Infrastructure & Ring Handlers): - Implement register_all() for all 14 domain crates with proper kernel registration - Complete KernelMessage derive macro with full RingMessage trait generation - Add state management to Ring kernels (PageRank, KMeans, VaR, GARCH, AML) - Implement Ring handlers with internal state using RwLock pattern Phase 7.3 (Financial Algorithms): - Implement full Black-Scholes option pricing with proper N(d1), N(d2) - Add delta-gamma VaR model for hedge effectiveness in FX hedging - Improve swap duration calculation with fixed vs floating leg analysis - Upgrade LCR improvement calculation to simulate actions and recalculate - Add proper days-until-breach liquidity runoff model Phase 7.4 (Statistical Algorithms): - Upgrade MA coefficient estimation from autocorrelation to CSS optimization - Implement proper Student's t-distribution p-value using regularized incomplete beta function with Lanczos log-gamma approximation Phase 7.5 (Business Logic): - Fix currency extraction in netting to use trade attributes - Implement full business hours processing with timezone support - Add configurable business hours start/end and timezone offset All 610 tests pass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4ae87d0 commit e9c4eed

93 files changed

Lines changed: 4630 additions & 1452 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rustkernel-accounting/src/coa_mapping.rs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,15 @@ impl ChartOfAccountsMapping {
178178
amount_ratio: 1.0,
179179
}]
180180
}
181-
MappingTransformation::Split(splits) => {
182-
splits
183-
.iter()
184-
.map(|(target, ratio)| MappedAccount {
185-
source_code: account.code.clone(),
186-
target_code: target.clone(),
187-
rule_id: rule.id.clone(),
188-
amount_ratio: *ratio,
189-
})
190-
.collect()
191-
}
181+
MappingTransformation::Split(splits) => splits
182+
.iter()
183+
.map(|(target, ratio)| MappedAccount {
184+
source_code: account.code.clone(),
185+
target_code: target.clone(),
186+
rule_id: rule.id.clone(),
187+
amount_ratio: *ratio,
188+
})
189+
.collect(),
192190
MappingTransformation::Aggregate => {
193191
vec![MappedAccount {
194192
source_code: account.code.clone(),
@@ -197,7 +195,11 @@ impl ChartOfAccountsMapping {
197195
amount_ratio: 1.0,
198196
}]
199197
}
200-
MappingTransformation::Conditional { condition, if_true, if_false } => {
198+
MappingTransformation::Conditional {
199+
condition,
200+
if_true,
201+
if_false,
202+
} => {
201203
let target = if Self::evaluate_condition(account, condition) {
202204
if_true.clone()
203205
} else {
@@ -253,7 +255,9 @@ impl ChartOfAccountsMapping {
253255
}
254256

255257
// Check for empty targets
256-
if rule.target_code.is_empty() && !matches!(rule.transformation, MappingTransformation::Split(_)) {
258+
if rule.target_code.is_empty()
259+
&& !matches!(rule.transformation, MappingTransformation::Split(_))
260+
{
257261
errors.push(RuleValidationError {
258262
rule_id: rule.id.clone(),
259263
message: "Target code is empty".to_string(),
@@ -443,7 +447,8 @@ mod tests {
443447
]),
444448
}];
445449

446-
let result = ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
450+
let result =
451+
ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
447452

448453
assert_eq!(result.mapped.len(), 2);
449454
assert!((result.mapped[0].amount_ratio - 0.6).abs() < 0.001);
@@ -484,7 +489,8 @@ mod tests {
484489
transformation: MappingTransformation::Direct,
485490
}];
486491

487-
let result = ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
492+
let result =
493+
ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
488494

489495
assert_eq!(result.stats.mapped_count, 1);
490496
assert_eq!(result.unmapped.len(), 1);
@@ -561,7 +567,8 @@ mod tests {
561567
let rules = create_test_rules();
562568

563569
// Default: exclude inactive
564-
let result1 = ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
570+
let result1 =
571+
ChartOfAccountsMapping::map_accounts(&accounts, &rules, &MappingConfig::default());
565572
assert_eq!(result1.stats.total_accounts, 0);
566573

567574
// Include inactive

crates/rustkernel-accounting/src/journal.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,8 @@ impl JournalTransformation {
5454
let mut total_credit = 0.0;
5555

5656
// Build mapping lookup
57-
let mapping_lookup: HashMap<String, Vec<&MappedAccount>> = mapping
58-
.mapped
59-
.iter()
60-
.fold(HashMap::new(), |mut acc, m| {
57+
let mapping_lookup: HashMap<String, Vec<&MappedAccount>> =
58+
mapping.mapped.iter().fold(HashMap::new(), |mut acc, m| {
6159
acc.entry(m.source_code.clone()).or_default().push(m);
6260
acc
6361
});
@@ -261,7 +259,8 @@ impl JournalTransformation {
261259

262260
// Count distinct entries per account
263261
for entry in entries {
264-
let mut seen_accounts: std::collections::HashSet<&str> = std::collections::HashSet::new();
262+
let mut seen_accounts: std::collections::HashSet<&str> =
263+
std::collections::HashSet::new();
265264
for line in &entry.lines {
266265
if seen_accounts.insert(&line.account_code) {
267266
if let Some(summary) = summaries.get_mut(&line.account_code) {
@@ -504,7 +503,12 @@ mod tests {
504503

505504
// Should preserve unmapped 4000 account
506505
assert_eq!(result.stats.transformed_count, 1);
507-
assert!(result.entries[0].lines.iter().any(|l| l.account_code == "4000"));
506+
assert!(
507+
result.entries[0]
508+
.lines
509+
.iter()
510+
.any(|l| l.account_code == "4000")
511+
);
508512
}
509513

510514
#[test]
@@ -541,12 +545,17 @@ mod tests {
541545
},
542546
};
543547

544-
let result = JournalTransformation::transform(&entries, &mapping, &TransformConfig::default());
548+
let result =
549+
JournalTransformation::transform(&entries, &mapping, &TransformConfig::default());
545550

546551
// Should have 3 lines (1000 split to 2, 4000 to 1)
547552
assert_eq!(result.entries[0].lines.len(), 3);
548553

549-
let a1001_line = result.entries[0].lines.iter().find(|l| l.account_code == "A1001").unwrap();
554+
let a1001_line = result.entries[0]
555+
.lines
556+
.iter()
557+
.find(|l| l.account_code == "A1001")
558+
.unwrap();
550559
assert!((a1001_line.debit - 600.0).abs() < 0.01);
551560
}
552561

crates/rustkernel-accounting/src/lib.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,59 @@
1111
1212
#![warn(missing_docs)]
1313

14-
pub mod types;
1514
pub mod coa_mapping;
1615
pub mod journal;
17-
pub mod reconciliation;
1816
pub mod network;
17+
pub mod reconciliation;
1918
pub mod temporal;
19+
pub mod types;
2020

2121
pub use coa_mapping::ChartOfAccountsMapping;
2222
pub use journal::JournalTransformation;
23-
pub use reconciliation::GLReconciliation;
2423
pub use network::NetworkAnalysis;
24+
pub use reconciliation::GLReconciliation;
2525
pub use temporal::TemporalCorrelation;
2626

2727
/// Register all accounting kernels.
28-
pub fn register_all(_registry: &rustkernel_core::registry::KernelRegistry) -> rustkernel_core::error::Result<()> {
28+
pub fn register_all(
29+
registry: &rustkernel_core::registry::KernelRegistry,
30+
) -> rustkernel_core::error::Result<()> {
31+
use rustkernel_core::traits::GpuKernel;
32+
2933
tracing::info!("Registering accounting kernels");
34+
35+
// CoA mapping kernel (1)
36+
registry.register_metadata(
37+
coa_mapping::ChartOfAccountsMapping::new()
38+
.metadata()
39+
.clone(),
40+
)?;
41+
42+
// Journal kernel (1)
43+
registry.register_metadata(journal::JournalTransformation::new().metadata().clone())?;
44+
45+
// Reconciliation kernel (1)
46+
registry.register_metadata(reconciliation::GLReconciliation::new().metadata().clone())?;
47+
48+
// Network kernel (1)
49+
registry.register_metadata(network::NetworkAnalysis::new().metadata().clone())?;
50+
51+
// Temporal kernel (1)
52+
registry.register_metadata(temporal::TemporalCorrelation::new().metadata().clone())?;
53+
54+
tracing::info!("Registered 5 accounting kernels");
3055
Ok(())
3156
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
use rustkernel_core::registry::KernelRegistry;
62+
63+
#[test]
64+
fn test_register_all() {
65+
let registry = KernelRegistry::new();
66+
register_all(&registry).expect("Failed to register accounting kernels");
67+
assert_eq!(registry.total_count(), 5);
68+
}
69+
}

crates/rustkernel-accounting/src/network.rs

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
//! - Generate elimination entries
77
88
use crate::types::{
9-
CircularReference, EliminationEntry, EntityBalance, EntityRelationship,
10-
IntercompanyStatus, IntercompanyTransaction, IntercompanyType, NetworkAnalysisResult,
11-
NetworkStats,
9+
CircularReference, EliminationEntry, EntityBalance, EntityRelationship, IntercompanyStatus,
10+
IntercompanyTransaction, IntercompanyType, NetworkAnalysisResult, NetworkStats,
1211
};
1312
use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
1413
use std::collections::{HashMap, HashSet};
@@ -94,27 +93,29 @@ impl NetworkAnalysis {
9493
}
9594

9695
// From entity has a receivable
97-
let from_balance = balances.entry(txn.from_entity.clone()).or_insert_with(|| {
98-
EntityBalance {
99-
entity_id: txn.from_entity.clone(),
100-
total_receivables: 0.0,
101-
total_payables: 0.0,
102-
net_position: 0.0,
103-
counterparty_count: 0,
104-
}
105-
});
96+
let from_balance =
97+
balances
98+
.entry(txn.from_entity.clone())
99+
.or_insert_with(|| EntityBalance {
100+
entity_id: txn.from_entity.clone(),
101+
total_receivables: 0.0,
102+
total_payables: 0.0,
103+
net_position: 0.0,
104+
counterparty_count: 0,
105+
});
106106
from_balance.total_receivables += txn.amount;
107107

108108
// To entity has a payable
109-
let to_balance = balances.entry(txn.to_entity.clone()).or_insert_with(|| {
110-
EntityBalance {
111-
entity_id: txn.to_entity.clone(),
112-
total_receivables: 0.0,
113-
total_payables: 0.0,
114-
net_position: 0.0,
115-
counterparty_count: 0,
116-
}
117-
});
109+
let to_balance =
110+
balances
111+
.entry(txn.to_entity.clone())
112+
.or_insert_with(|| EntityBalance {
113+
entity_id: txn.to_entity.clone(),
114+
total_receivables: 0.0,
115+
total_payables: 0.0,
116+
net_position: 0.0,
117+
counterparty_count: 0,
118+
});
118119
to_balance.total_payables += txn.amount;
119120
}
120121

@@ -166,15 +167,15 @@ impl NetworkAnalysis {
166167
(txn.to_entity.clone(), txn.from_entity.clone())
167168
};
168169

169-
let rel = relationships.entry(key.clone()).or_insert_with(|| {
170-
EntityRelationship {
170+
let rel = relationships
171+
.entry(key.clone())
172+
.or_insert_with(|| EntityRelationship {
171173
from_entity: key.0.clone(),
172174
to_entity: key.1.clone(),
173175
total_volume: 0.0,
174176
transaction_count: 0,
175177
net_balance: 0.0,
176-
}
177-
});
178+
});
178179

179180
rel.total_volume += txn.amount;
180181
rel.transaction_count += 1;
@@ -259,7 +260,10 @@ impl NetworkAnalysis {
259260
.windows(2)
260261
.filter_map(|w| {
261262
graph.get(&w[0]).and_then(|edges| {
262-
edges.iter().find(|(to, _)| to == &w[1]).map(|(_, amt)| *amt)
263+
edges
264+
.iter()
265+
.find(|(to, _)| to == &w[1])
266+
.map(|(_, amt)| *amt)
263267
})
264268
})
265269
.sum();
@@ -298,7 +302,8 @@ impl NetworkAnalysis {
298302
continue;
299303
}
300304

301-
let (debit_account, credit_account) = Self::get_elimination_accounts(&txn.transaction_type);
305+
let (debit_account, credit_account) =
306+
Self::get_elimination_accounts(&txn.transaction_type);
302307

303308
eliminations.push(EliminationEntry {
304309
id: format!("ELIM{:05}", entry_id),
@@ -320,18 +325,30 @@ impl NetworkAnalysis {
320325
fn get_elimination_accounts(txn_type: &IntercompanyType) -> (String, String) {
321326
match txn_type {
322327
IntercompanyType::Trade => ("IC_PAYABLES".to_string(), "IC_RECEIVABLES".to_string()),
323-
IntercompanyType::Loan => ("IC_LOAN_PAYABLE".to_string(), "IC_LOAN_RECEIVABLE".to_string()),
324-
IntercompanyType::Dividend => ("DIVIDEND_INCOME".to_string(), "DIVIDEND_EXPENSE".to_string()),
325-
IntercompanyType::ManagementFee => ("MGMT_FEE_INCOME".to_string(), "MGMT_FEE_EXPENSE".to_string()),
326-
IntercompanyType::Royalty => ("ROYALTY_INCOME".to_string(), "ROYALTY_EXPENSE".to_string()),
327-
IntercompanyType::Other => ("IC_OTHER_PAYABLE".to_string(), "IC_OTHER_RECEIVABLE".to_string()),
328+
IntercompanyType::Loan => (
329+
"IC_LOAN_PAYABLE".to_string(),
330+
"IC_LOAN_RECEIVABLE".to_string(),
331+
),
332+
IntercompanyType::Dividend => (
333+
"DIVIDEND_INCOME".to_string(),
334+
"DIVIDEND_EXPENSE".to_string(),
335+
),
336+
IntercompanyType::ManagementFee => (
337+
"MGMT_FEE_INCOME".to_string(),
338+
"MGMT_FEE_EXPENSE".to_string(),
339+
),
340+
IntercompanyType::Royalty => {
341+
("ROYALTY_INCOME".to_string(), "ROYALTY_EXPENSE".to_string())
342+
}
343+
IntercompanyType::Other => (
344+
"IC_OTHER_PAYABLE".to_string(),
345+
"IC_OTHER_RECEIVABLE".to_string(),
346+
),
328347
}
329348
}
330349

331350
/// Calculate netting opportunities.
332-
pub fn calculate_netting(
333-
transactions: &[IntercompanyTransaction],
334-
) -> Vec<NettingOpportunity> {
351+
pub fn calculate_netting(transactions: &[IntercompanyTransaction]) -> Vec<NettingOpportunity> {
335352
let mut opportunities = Vec::new();
336353

337354
// Find bilateral netting opportunities
@@ -550,9 +567,10 @@ mod tests {
550567
assert!(!result.elimination_entries.is_empty());
551568

552569
// Check trade elimination
553-
let trade_elim = result.elimination_entries.iter().find(|e| {
554-
e.from_entity == "CORP_A" && e.to_entity == "CORP_B"
555-
});
570+
let trade_elim = result
571+
.elimination_entries
572+
.iter()
573+
.find(|e| e.from_entity == "CORP_A" && e.to_entity == "CORP_B");
556574
assert!(trade_elim.is_some());
557575
}
558576

0 commit comments

Comments
 (0)