Skip to main content

qualia_core_db/specialized_libs/financial_modeling/
compliance.rs

1use super::*;
2
3/// Internal verdict from evaluating a single compliance rule.
4struct RuleVerdict {
5    passed: bool,
6    flagged: bool,
7    message: String,
8    recommendation: String,
9}
10
11/// Compliance monitor
12pub struct ComplianceMonitor {
13    compliance_rules: HashMap<String, ComplianceRule>,
14    surveillance_engine: SurveillanceEngine,
15    reporting_engine: ReportingEngine,
16}
17
18/// Surveillance engine
19pub struct SurveillanceEngine {
20    surveillance_rules: HashMap<String, SurveillanceRule>,
21    anomaly_detector: AnomalyDetector,
22    alert_manager: AlertManager,
23}
24
25/// Surveillance rules
26#[derive(Debug, Clone)]
27pub struct SurveillanceRule {
28    pub rule_id: String,
29    pub rule_name: String,
30    pub rule_type: SurveillanceRuleType,
31    pub conditions: Vec<SurveillanceCondition>,
32    pub actions: Vec<SurveillanceAction>,
33}
34
35/// Surveillance rule types
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub enum SurveillanceRuleType {
38    MarketManipulation,
39    InsiderTrading,
40    FrontRunning,
41    BestExecution,
42    TradeReporting,
43}
44
45/// Surveillance conditions
46#[derive(Debug, Clone)]
47pub struct SurveillanceCondition {
48    pub condition_id: String,
49    pub field: String,
50    pub operator: ComparisonOperator,
51    pub value: SurveillanceValue,
52}
53
54/// Surveillance values
55#[derive(Debug, Clone)]
56pub enum SurveillanceValue {
57    String(String),
58    Number(f64),
59    Boolean(bool),
60}
61
62/// Surveillance actions
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub enum SurveillanceAction {
65    Alert,
66    Block,
67    Escalate,
68    Report,
69}
70
71/// Anomaly detector
72pub struct AnomalyDetector {
73    detection_algorithms: HashMap<String, DetectionAlgorithm>,
74    anomaly_patterns: HashMap<String, AnomalyPattern>,
75}
76
77/// Detection algorithms
78#[derive(Debug, Clone)]
79pub struct DetectionAlgorithm {
80    pub algorithm_id: String,
81    pub algorithm_type: DetectionAlgorithmType,
82    pub parameters: DetectionAlgorithmParameters,
83}
84
85/// Detection algorithm types
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub enum DetectionAlgorithmType {
88    Statistical,
89    MachineLearning,
90    RuleBased,
91    Hybrid,
92}
93
94/// Detection algorithm parameters
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct DetectionAlgorithmParameters {
97    pub confidence_threshold: f64,
98    pub sensitivity: f64,
99    pub lookback_period: u32,
100}
101
102/// Anomaly patterns
103#[derive(Debug, Clone)]
104pub struct AnomalyPattern {
105    pub pattern_id: String,
106    pub pattern_name: String,
107    pub pattern_type: AnomalyPatternType,
108    pub characteristics: Vec<String>,
109}
110
111/// Anomaly pattern types
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub enum AnomalyPatternType {
114    Price,
115    Volume,
116    Timing,
117    Sequence,
118}
119
120/// Alert manager
121pub struct AlertManager {
122    alerts: HashMap<String, Alert>,
123    alert_escalation: AlertEscalation,
124    notification_system: NotificationSystem,
125}
126
127/// Alerts
128#[derive(Debug, Clone)]
129pub struct Alert {
130    pub alert_id: String,
131    pub alert_type: AlertType,
132    pub severity: AlertSeverity,
133    pub description: String,
134    pub source: String,
135    pub timestamp: u64,
136    pub status: AlertStatus,
137}
138
139/// Alert types
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub enum AlertType {
142    Compliance,
143    Risk,
144    Operational,
145    Security,
146}
147
148/// Alert severity
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub enum AlertSeverity {
151    Low,
152    Medium,
153    High,
154    Critical,
155}
156
157/// Alert status
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub enum AlertStatus {
160    New,
161    Acknowledged,
162    Investigating,
163    Resolved,
164    Closed,
165}
166
167/// Alert escalation
168pub struct AlertEscalation {
169    escalation_rules: HashMap<String, EscalationRule>,
170    escalation_history: HashMap<String, EscalationHistory>,
171}
172
173/// Escalation rules
174#[derive(Debug, Clone)]
175pub struct EscalationRule {
176    pub rule_id: String,
177    pub conditions: Vec<EscalationCondition>,
178    pub actions: Vec<EscalationAction>,
179}
180
181/// Escalation conditions
182#[derive(Debug, Clone)]
183pub struct EscalationCondition {
184    pub condition_id: String,
185    pub field: String,
186    pub operator: ComparisonOperator,
187    pub value: EscalationValue,
188}
189
190/// Escalation values
191#[derive(Debug, Clone)]
192pub enum EscalationValue {
193    String(String),
194    Number(f64),
195    Boolean(bool),
196}
197
198/// Escalation actions
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub enum EscalationAction {
201    Notify,
202    Escalate,
203    Block,
204    Report,
205}
206
207/// Escalation history
208#[derive(Debug, Clone)]
209pub struct EscalationHistory {
210    pub history_id: String,
211    pub alert_id: String,
212    pub escalation_steps: Vec<EscalationStep>,
213}
214
215/// Escalation steps
216#[derive(Debug, Clone)]
217pub struct EscalationStep {
218    pub step_id: String,
219    pub action: EscalationAction,
220    pub timestamp: u64,
221    pub performed_by: String,
222}
223
224/// Notification system
225pub struct NotificationSystem {
226    notification_channels: HashMap<String, NotificationChannel>,
227    notification_templates: HashMap<String, NotificationTemplate>,
228}
229
230/// Notification channels
231#[derive(Debug, Clone)]
232pub struct NotificationChannel {
233    pub channel_id: String,
234    pub channel_type: NotificationChannelType,
235    pub configuration: ChannelConfiguration,
236}
237
238/// Notification channel types
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub enum NotificationChannelType {
241    Email,
242    SMS,
243    Slack,
244    Webhook,
245    InApp,
246}
247
248/// Channel configuration
249#[derive(Debug, Clone)]
250pub struct ChannelConfiguration {
251    pub endpoint: String,
252    pub authentication: AuthenticationMethod,
253    pub format: NotificationFormat,
254}
255
256/// Notification formats
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub enum NotificationFormat {
259    Text,
260    HTML,
261    JSON,
262    Custom,
263}
264
265/// Notification templates
266#[derive(Debug, Clone)]
267pub struct NotificationTemplate {
268    pub template_id: String,
269    pub template_name: String,
270    pub template_type: NotificationTemplateType,
271    pub content: String,
272}
273
274/// Notification template types
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub enum NotificationTemplateType {
277    Alert,
278    Report,
279    Summary,
280    Custom,
281}
282
283impl ComplianceMonitor {
284    pub fn new() -> Self {
285        Self {
286            compliance_rules: HashMap::new(),
287            surveillance_engine: SurveillanceEngine::new(),
288            reporting_engine: ReportingEngine::new(),
289        }
290    }
291
292    pub fn initialize(&mut self) -> Result<(), FinancialError> {
293        self.surveillance_engine.initialize()?;
294        self.reporting_engine.initialize()?;
295        Ok(())
296    }
297
298    /// Register a compliance rule. Rules are keyed by `rule_id`; registering
299    /// with a duplicate id replaces the prior rule.
300    pub fn add_rule(&mut self, rule: ComplianceRule) {
301        self.compliance_rules.insert(rule.rule_id.clone(), rule);
302    }
303
304    /// Number of registered compliance rules.
305    pub fn rule_count(&self) -> usize {
306        self.compliance_rules.len()
307    }
308
309    /// Evaluate every registered compliance rule against the portfolio.
310    ///
311    /// Returns a `ComplianceResult` with:
312    /// - `status: Compliant` when every rule passes (or no rules are registered
313    ///   *and* the portfolio has no positions — an empty portfolio with no
314    ///   rules is trivially compliant; a non-empty portfolio with no rules is
315    ///   `Flagged` because compliance was never asserted).
316    /// - `status: NonCompliant` when one or more rules fail.
317    /// - `status: Flagged` when a rule neither clearly passes nor fails but
318    ///   warrants review (e.g. a `Custom` rule with no check).
319    /// - `risk_score`: fraction of rules that failed (0.0 = all pass, 1.0 = all fail).
320    /// - `violations`: human-readable descriptions of each failed rule.
321    /// - `recommendations`: suggested remediation per failed rule.
322    /// - `audit_entries`: one entry per evaluated rule.
323    pub fn check_compliance(
324        &mut self,
325        portfolio: &Portfolio,
326    ) -> Result<ComplianceResult, FinancialError> {
327        let mut violations = Vec::new();
328        let mut recommendations = Vec::new();
329        let mut audit_entries = Vec::new();
330        let mut failed = 0usize;
331        let mut flagged = 0usize;
332        let now = portfolio.last_updated;
333
334        // Special case: no rules registered.
335        if self.compliance_rules.is_empty() {
336            if portfolio.assets.is_empty() {
337                // Empty portfolio, no rules → trivially compliant.
338                return Ok(ComplianceResult {
339                    result_id: format!("compliance_{}_{}", portfolio.portfolio_id, now),
340                    portfolio_id: portfolio.portfolio_id.clone(),
341                    status: ComplianceStatus::Compliant,
342                    risk_score: 0.0,
343                    violations: Vec::new(),
344                    recommendations: Vec::new(),
345                    audit_entries: Vec::new(),
346                });
347            }
348            // Non-empty portfolio, no rules → we cannot assert compliance.
349            return Ok(ComplianceResult {
350                result_id: format!("compliance_{}_{}", portfolio.portfolio_id, now),
351                portfolio_id: portfolio.portfolio_id.clone(),
352                status: ComplianceStatus::Flagged,
353                risk_score: 1.0,
354                violations: vec![
355                    "No compliance rules registered — cannot assert compliance for a non-empty portfolio."
356                        .to_string(),
357                ],
358                recommendations: vec![
359                    "Register compliance rules (position limits, KYC/AML, margin, trading restrictions) before asserting compliance."
360                        .to_string(),
361                ],
362                audit_entries: Vec::new(),
363            });
364        }
365
366        let total_rules = self.compliance_rules.len();
367
368        // Evaluate each rule. Sort by rule_id for deterministic ordering.
369        let mut rule_ids: Vec<&String> = self.compliance_rules.keys().collect();
370        rule_ids.sort();
371
372        for rule_id in rule_ids {
373            let rule = &self.compliance_rules[rule_id];
374            let verdict = Self::evaluate_rule(rule, portfolio);
375
376            audit_entries.push(AuditEntry {
377                entry_id: format!("audit_{}_{}", rule.rule_id, now),
378                timestamp: now,
379                user_id: String::new(),
380                portfolio_id: portfolio.portfolio_id.clone(),
381                action: PortfolioAction::ComplianceCheck,
382                details: format!(
383                    "Rule '{}' ({}): {} — {}",
384                    rule.rule_id,
385                    rule.rule_type_as_str(),
386                    if verdict.passed { "PASS" } else { "FAIL" },
387                    verdict.message
388                ),
389                ip_address: String::new(),
390            });
391
392            if !verdict.passed {
393                failed += 1;
394                violations.push(format!("{}: {}", rule.rule_id, verdict.message));
395                recommendations.push(verdict.recommendation);
396            } else if verdict.flagged {
397                flagged += 1;
398            }
399        }
400
401        let risk_score = failed as f64 / total_rules as f64;
402
403        let status = if failed > 0 {
404            ComplianceStatus::NonCompliant
405        } else if flagged > 0 {
406            ComplianceStatus::Flagged
407        } else {
408            ComplianceStatus::Compliant
409        };
410
411        Ok(ComplianceResult {
412            result_id: format!("compliance_{}_{}", portfolio.portfolio_id, now),
413            portfolio_id: portfolio.portfolio_id.clone(),
414            status,
415            risk_score,
416            violations,
417            recommendations,
418            audit_entries,
419        })
420    }
421
422    /// Evaluate a single compliance rule against a portfolio.
423    fn evaluate_rule(rule: &ComplianceRule, portfolio: &Portfolio) -> RuleVerdict {
424        match rule.rule_type {
425            ComplianceRuleType::PositionLimit => {
426                let max_position = rule.parameters.get("max_position").copied().unwrap_or(0.0);
427                if max_position <= 0.0 {
428                    return RuleVerdict {
429                        passed: false,
430                        flagged: false,
431                        message: "PositionLimit rule has no max_position parameter".to_string(),
432                        recommendation: "Set the 'max_position' parameter to a positive value."
433                            .to_string(),
434                    };
435                }
436                // Check each asset's market value against the limit.
437                for asset in &portfolio.assets {
438                    if asset.market_value > max_position {
439                        return RuleVerdict {
440                            passed: false,
441                            flagged: false,
442                            message: format!(
443                                "Asset {} market value {:.2} exceeds max_position {:.2}",
444                                asset.symbol, asset.market_value, max_position
445                            ),
446                            recommendation: format!(
447                                "Reduce position in {} to at most {:.2}",
448                                asset.symbol, max_position
449                            ),
450                        };
451                    }
452                }
453                RuleVerdict {
454                    passed: true,
455                    flagged: false,
456                    message: "All positions within limit".to_string(),
457                    recommendation: String::new(),
458                }
459            }
460            ComplianceRuleType::KYC => {
461                let kyc_required = rule.parameters.get("kyc_required").copied().unwrap_or(1.0);
462                if kyc_required >= 1.0 {
463                    // In a real system, this would check the portfolio owner's KYC status
464                    // from an identity registry. Here we check the risk_profile as a proxy:
465                    // an Unverified profile fails KYC.
466                    let verified = matches!(
467                        portfolio.risk_profile.risk_tolerance,
468                        RiskTolerance::Conservative
469                            | RiskTolerance::Moderate
470                            | RiskTolerance::Aggressive
471                            | RiskTolerance::VeryAggressive
472                    ) && !portfolio.owner_id.is_empty();
473                    if !verified {
474                        return RuleVerdict {
475                            passed: false,
476                            flagged: false,
477                            message: "KYC verification required but owner identity not verified"
478                                .to_string(),
479                            recommendation: "Complete KYC verification before trading.".to_string(),
480                        };
481                    }
482                }
483                RuleVerdict {
484                    passed: true,
485                    flagged: false,
486                    message: "KYC verified".to_string(),
487                    recommendation: String::new(),
488                }
489            }
490            ComplianceRuleType::AML => {
491                let aml_required = rule.parameters.get("kyc_required").copied().unwrap_or(1.0);
492                if aml_required >= 1.0 && portfolio.owner_id.is_empty() {
493                    return RuleVerdict {
494                        passed: false,
495                        flagged: false,
496                        message: "AML clearance required but no owner identified".to_string(),
497                        recommendation: "Provide owner identification for AML screening."
498                            .to_string(),
499                    };
500                }
501                RuleVerdict {
502                    passed: true,
503                    flagged: false,
504                    message: "AML cleared".to_string(),
505                    recommendation: String::new(),
506                }
507            }
508            ComplianceRuleType::MarginRequirement => {
509                let margin_pct = rule.parameters.get("margin_pct").copied().unwrap_or(0.0);
510                if margin_pct <= 0.0 {
511                    return RuleVerdict {
512                        passed: false,
513                        flagged: false,
514                        message: "MarginRequirement rule has no margin_pct parameter".to_string(),
515                        recommendation: "Set the 'margin_pct' parameter to a positive value."
516                            .to_string(),
517                    };
518                }
519                let required_margin = portfolio.total_value * margin_pct / 100.0;
520                if portfolio.cash_balance < required_margin {
521                    return RuleVerdict {
522                        passed: false,
523                        flagged: false,
524                        message: format!(
525                            "Cash balance {:.2} below required margin {:.2} ({:.1}% of {:.2})",
526                            portfolio.cash_balance,
527                            required_margin,
528                            margin_pct,
529                            portfolio.total_value
530                        ),
531                        recommendation: format!(
532                            "Increase cash balance to at least {:.2} to meet margin requirement.",
533                            required_margin
534                        ),
535                    };
536                }
537                RuleVerdict {
538                    passed: true,
539                    flagged: false,
540                    message: format!(
541                        "Margin satisfied: {:.2} >= {:.2}",
542                        portfolio.cash_balance, required_margin
543                    ),
544                    recommendation: String::new(),
545                }
546            }
547            ComplianceRuleType::TradingRestriction => {
548                let restricted = rule
549                    .string_parameters
550                    .get("restricted_assets")
551                    .map(|s| s.as_str())
552                    .unwrap_or("");
553                if restricted.is_empty() {
554                    return RuleVerdict {
555                        passed: true,
556                        flagged: true,
557                        message: "TradingRestriction rule has no restricted_assets list — no assets restricted".to_string(),
558                        recommendation: "Populate 'restricted_assets' if trading restrictions are intended.".to_string(),
559                    };
560                }
561                let restricted_set: Vec<&str> = restricted.split(',').map(|s| s.trim()).collect();
562                for asset in &portfolio.assets {
563                    if restricted_set.contains(&asset.symbol.as_str()) {
564                        return RuleVerdict {
565                            passed: false,
566                            flagged: false,
567                            message: format!("Asset {} is on the restricted list", asset.symbol),
568                            recommendation: format!("Divest restricted asset {}.", asset.symbol),
569                        };
570                    }
571                }
572                RuleVerdict {
573                    passed: true,
574                    flagged: false,
575                    message: "No restricted assets held".to_string(),
576                    recommendation: String::new(),
577                }
578            }
579            ComplianceRuleType::Custom => {
580                // Custom rules always pass by default — they have no built-in check.
581                RuleVerdict {
582                    passed: true,
583                    flagged: true,
584                    message: "Custom rule — no built-in check, passes by default".to_string(),
585                    recommendation: "Implement a custom evaluator if enforcement is needed."
586                        .to_string(),
587                }
588            }
589        }
590    }
591}
592
593impl SurveillanceEngine {
594    pub fn new() -> Self {
595        Self {
596            surveillance_rules: HashMap::new(),
597            anomaly_detector: AnomalyDetector::new(),
598            alert_manager: AlertManager::new(),
599        }
600    }
601
602    pub fn initialize(&mut self) -> Result<(), FinancialError> {
603        self.anomaly_detector.initialize()?;
604        self.alert_manager.initialize()?;
605        Ok(())
606    }
607
608    pub fn add_surveillance_rule(&mut self, rule: SurveillanceRule) {
609        self.surveillance_rules.insert(rule.rule_id.clone(), rule);
610    }
611
612    pub fn get_surveillance_rule(&self, rule_id: &str) -> Option<&SurveillanceRule> {
613        self.surveillance_rules.get(rule_id)
614    }
615
616    pub fn list_surveillance_rules(&self) -> Vec<String> {
617        self.surveillance_rules.keys().cloned().collect()
618    }
619}
620
621impl AnomalyDetector {
622    pub fn new() -> Self {
623        Self {
624            detection_algorithms: HashMap::new(),
625            anomaly_patterns: HashMap::new(),
626        }
627    }
628
629    pub fn initialize(&mut self) -> Result<(), FinancialError> {
630        Ok(())
631    }
632
633    pub fn add_detection_algorithm(&mut self, algorithm: DetectionAlgorithm) {
634        self.detection_algorithms
635            .insert(algorithm.algorithm_id.clone(), algorithm);
636    }
637
638    pub fn get_detection_algorithm(&self, algorithm_id: &str) -> Option<&DetectionAlgorithm> {
639        self.detection_algorithms.get(algorithm_id)
640    }
641
642    pub fn list_detection_algorithms(&self) -> Vec<String> {
643        self.detection_algorithms.keys().cloned().collect()
644    }
645
646    pub fn add_anomaly_pattern(&mut self, pattern: AnomalyPattern) {
647        self.anomaly_patterns
648            .insert(pattern.pattern_id.clone(), pattern);
649    }
650
651    pub fn get_anomaly_pattern(&self, pattern_id: &str) -> Option<&AnomalyPattern> {
652        self.anomaly_patterns.get(pattern_id)
653    }
654
655    pub fn list_anomaly_patterns(&self) -> Vec<String> {
656        self.anomaly_patterns.keys().cloned().collect()
657    }
658}
659
660impl AlertManager {
661    pub fn new() -> Self {
662        Self {
663            alerts: HashMap::new(),
664            alert_escalation: AlertEscalation::new(),
665            notification_system: NotificationSystem::new(),
666        }
667    }
668
669    pub fn initialize(&mut self) -> Result<(), FinancialError> {
670        Ok(())
671    }
672
673    pub fn add_alert(&mut self, alert: Alert) {
674        self.alerts.insert(alert.alert_id.clone(), alert);
675    }
676
677    pub fn get_alert(&self, alert_id: &str) -> Option<&Alert> {
678        self.alerts.get(alert_id)
679    }
680
681    pub fn list_alerts(&self) -> Vec<String> {
682        self.alerts.keys().cloned().collect()
683    }
684
685    pub fn alert_escalation(&self) -> &AlertEscalation {
686        &self.alert_escalation
687    }
688
689    pub fn alert_escalation_mut(&mut self) -> &mut AlertEscalation {
690        &mut self.alert_escalation
691    }
692
693    pub fn notification_system(&self) -> &NotificationSystem {
694        &self.notification_system
695    }
696
697    pub fn notification_system_mut(&mut self) -> &mut NotificationSystem {
698        &mut self.notification_system
699    }
700}
701
702impl AlertEscalation {
703    pub fn new() -> Self {
704        Self {
705            escalation_rules: HashMap::new(),
706            escalation_history: HashMap::new(),
707        }
708    }
709
710    pub fn add_escalation_rule(&mut self, rule: EscalationRule) {
711        self.escalation_rules.insert(rule.rule_id.clone(), rule);
712    }
713
714    pub fn get_escalation_rule(&self, rule_id: &str) -> Option<&EscalationRule> {
715        self.escalation_rules.get(rule_id)
716    }
717
718    pub fn list_escalation_rules(&self) -> Vec<String> {
719        self.escalation_rules.keys().cloned().collect()
720    }
721
722    pub fn add_escalation_history(&mut self, history: EscalationHistory) {
723        self.escalation_history
724            .insert(history.history_id.clone(), history);
725    }
726
727    pub fn get_escalation_history(&self, history_id: &str) -> Option<&EscalationHistory> {
728        self.escalation_history.get(history_id)
729    }
730
731    pub fn list_escalation_history(&self) -> Vec<String> {
732        self.escalation_history.keys().cloned().collect()
733    }
734}
735
736impl NotificationSystem {
737    pub fn new() -> Self {
738        Self {
739            notification_channels: HashMap::new(),
740            notification_templates: HashMap::new(),
741        }
742    }
743
744    pub fn add_channel(&mut self, channel: NotificationChannel) {
745        self.notification_channels
746            .insert(channel.channel_id.clone(), channel);
747    }
748
749    pub fn get_channel(&self, channel_id: &str) -> Option<&NotificationChannel> {
750        self.notification_channels.get(channel_id)
751    }
752
753    pub fn list_channels(&self) -> Vec<String> {
754        self.notification_channels.keys().cloned().collect()
755    }
756
757    pub fn add_template(&mut self, template: NotificationTemplate) {
758        self.notification_templates
759            .insert(template.template_id.clone(), template);
760    }
761
762    pub fn get_template(&self, template_id: &str) -> Option<&NotificationTemplate> {
763        self.notification_templates.get(template_id)
764    }
765
766    pub fn list_templates(&self) -> Vec<String> {
767        self.notification_templates.keys().cloned().collect()
768    }
769}