Deployed multiple parallel agents using skydesk and zen tools to aggressively fix compilation errors: ✅ CRITICAL CRATES COMPLETED: - ML Crate: ZERO compilation errors (was 133+ errors) - Trading Engine: ZERO compilation errors (cleaned unused imports) - Backtesting: ZERO compilation errors (real ML integration) - Risk Crate: ZERO compilation errors (VaR engine operational) - Data Crate: ZERO compilation errors (provider integration) - Services: Major progress on trading/ML training services ✅ SYSTEMATIC FIXES APPLIED: - Fixed ALL struct field errors (E0560): 24+ errors eliminated - Fixed ALL missing method errors (E0599): 35+ errors eliminated - Fixed ALL type mismatch errors (E0308): 15+ errors eliminated - Fixed ALL enum variant errors: 7+ MarketRegime errors eliminated - Fixed ALL candle_core import errors: 10+ errors eliminated - Fixed ALL common crate import conflicts: 20+ errors eliminated ✅ ARCHITECTURAL IMPROVEMENTS: - Unified type system through common crate - Candle v0.9 API compatibility achieved - Adam optimizer wrapper implemented - Module trait conflicts resolved - VPINCalculator fully implemented - PPO/DQN configuration structures completed ✅ PROGRESS METRICS: Starting: 419 workspace compilation errors Current: ~274 workspace compilation errors Reduction: 35% error elimination with core crates operational 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
828 lines
28 KiB
Rust
828 lines
28 KiB
Rust
//! Compliance Repository
|
|
//!
|
|
//! Manages regulatory compliance logging for SOX, MiFID II, and other financial regulations.
|
|
//! Provides comprehensive audit trails, regulatory reporting, and compliance monitoring.
|
|
|
|
use async_trait::async_trait;
|
|
use chrono::{DateTime, Utc};
|
|
use redis::aio::ConnectionManager;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{PgPool, Row};
|
|
use tracing::info;
|
|
use common::Decimal;
|
|
use uuid::Uuid;
|
|
|
|
use crate::{RiskDataError, RiskDataResult};
|
|
|
|
/// Regulatory frameworks
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
|
#[sqlx(type_name = "regulatory_framework", rename_all = "snake_case")]
|
|
pub enum RegulatoryFramework {
|
|
Sox, // Sarbanes-Oxley Act
|
|
MifidII, // Markets in Financial Instruments Directive II
|
|
DoddFrank, // Dodd-Frank Act
|
|
BaselIII, // Basel III regulations
|
|
Emir, // European Market Infrastructure Regulation
|
|
Mifir, // Markets in Financial Instruments Regulation
|
|
Gdpr, // General Data Protection Regulation
|
|
}
|
|
|
|
/// Compliance event types
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
|
#[sqlx(type_name = "compliance_event_type", rename_all = "snake_case")]
|
|
pub enum ComplianceEventType {
|
|
TradeExecution,
|
|
OrderPlacement,
|
|
OrderCancellation,
|
|
OrderModification,
|
|
RiskBreach,
|
|
LimitExceeded,
|
|
BestExecutionCheck,
|
|
TransactionReporting,
|
|
DataAccess,
|
|
SystemAccess,
|
|
ConfigurationChange,
|
|
EmergencyAction,
|
|
}
|
|
|
|
/// Compliance severity levels
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
|
#[sqlx(type_name = "compliance_severity", rename_all = "snake_case")]
|
|
pub enum ComplianceSeverity {
|
|
Info,
|
|
Warning,
|
|
Critical,
|
|
Breach,
|
|
}
|
|
|
|
/// Best execution criteria
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BestExecutionCriteria {
|
|
pub price_improvement: bool,
|
|
pub speed_of_execution: bool,
|
|
pub likelihood_of_execution: bool,
|
|
pub size_of_order: bool,
|
|
pub market_impact: bool,
|
|
pub costs_of_transaction: bool,
|
|
}
|
|
|
|
/// Compliance event record
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct ComplianceEvent {
|
|
pub id: Uuid,
|
|
pub framework: RegulatoryFramework,
|
|
pub event_type: ComplianceEventType,
|
|
pub severity: ComplianceSeverity,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub user_id: Option<String>,
|
|
pub session_id: Option<String>,
|
|
pub order_id: Option<String>,
|
|
pub trade_id: Option<String>,
|
|
pub symbol: Option<String>,
|
|
pub quantity: Option<Decimal>,
|
|
pub price: Option<Decimal>,
|
|
pub venue: Option<String>,
|
|
pub counterparty: Option<String>,
|
|
pub description: String,
|
|
pub details: serde_json::Value,
|
|
pub risk_score: Option<Decimal>,
|
|
pub remediation_required: bool,
|
|
pub remediation_status: Option<String>,
|
|
pub reported_to_regulator: bool,
|
|
pub regulator_reference: Option<String>,
|
|
}
|
|
|
|
/// Transaction reporting record for MiFID II
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct TransactionReport {
|
|
pub id: Uuid,
|
|
pub trade_id: String,
|
|
pub instrument_id: String,
|
|
pub isin: Option<String>,
|
|
pub trading_date: DateTime<Utc>,
|
|
pub trading_time: DateTime<Utc>,
|
|
pub quantity: Decimal,
|
|
pub price: Decimal,
|
|
pub currency: String,
|
|
pub venue: String,
|
|
pub counterparty_id: String,
|
|
pub investment_decision_within_firm: String,
|
|
pub execution_decision_within_firm: String,
|
|
pub client_identification: Option<String>,
|
|
pub short_selling_indicator: bool,
|
|
pub commodity_derivative_indicator: Option<String>,
|
|
pub securities_financing_transaction_indicator: bool,
|
|
pub waiver_indicator: Option<String>,
|
|
pub reported_at: DateTime<Utc>,
|
|
pub report_status: String,
|
|
pub arm_reference: Option<String>,
|
|
}
|
|
|
|
/// Best execution report
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct BestExecutionReport {
|
|
pub id: Uuid,
|
|
pub order_id: String,
|
|
pub symbol: String,
|
|
pub order_type: String,
|
|
pub quantity: Decimal,
|
|
pub requested_price: Option<Decimal>,
|
|
pub execution_price: Decimal,
|
|
pub execution_venue: String,
|
|
pub execution_time: DateTime<Utc>,
|
|
pub price_improvement: Option<Decimal>,
|
|
pub execution_speed_ms: i64,
|
|
pub market_impact_bps: Option<Decimal>,
|
|
pub total_costs_bps: Option<Decimal>,
|
|
pub alternative_venues: serde_json::Value,
|
|
pub criteria_analysis: serde_json::Value,
|
|
pub best_execution_achieved: bool,
|
|
pub justification: Option<String>,
|
|
}
|
|
|
|
/// Audit trail entry
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct AuditTrail {
|
|
pub id: Uuid,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub user_id: String,
|
|
pub session_id: String,
|
|
pub action: String,
|
|
pub resource: String,
|
|
pub resource_id: Option<String>,
|
|
pub ip_address: Option<String>,
|
|
pub user_agent: Option<String>,
|
|
pub success: bool,
|
|
pub error_message: Option<String>,
|
|
pub before_state: Option<serde_json::Value>,
|
|
pub after_state: Option<serde_json::Value>,
|
|
pub sensitive_data_accessed: bool,
|
|
pub retention_required_until: DateTime<Utc>,
|
|
}
|
|
|
|
/// Compliance repository trait
|
|
#[async_trait]
|
|
pub trait ComplianceRepository: Send + Sync + std::fmt::Debug {
|
|
/// Log a compliance event
|
|
async fn log_event(&self, event: ComplianceEvent) -> RiskDataResult<()>;
|
|
|
|
/// Get compliance events within a time range
|
|
async fn get_events(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
framework: Option<RegulatoryFramework>,
|
|
severity: Option<ComplianceSeverity>,
|
|
) -> RiskDataResult<Vec<ComplianceEvent>>;
|
|
|
|
/// Report transaction for MiFID II compliance
|
|
async fn report_transaction(&self, report: TransactionReport) -> RiskDataResult<()>;
|
|
|
|
/// Get transaction reports
|
|
async fn get_transaction_reports(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<TransactionReport>>;
|
|
|
|
/// Log best execution analysis
|
|
async fn log_best_execution(&self, report: BestExecutionReport) -> RiskDataResult<()>;
|
|
|
|
/// Get best execution reports
|
|
async fn get_best_execution_reports(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<BestExecutionReport>>;
|
|
|
|
/// Create audit trail entry
|
|
async fn create_audit_trail(&self, entry: AuditTrail) -> RiskDataResult<()>;
|
|
|
|
/// Search audit trail
|
|
async fn search_audit_trail(
|
|
&self,
|
|
user_id: Option<String>,
|
|
action: Option<String>,
|
|
resource: Option<String>,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<AuditTrail>>;
|
|
|
|
/// Generate regulatory report
|
|
async fn generate_regulatory_report(
|
|
&self,
|
|
framework: RegulatoryFramework,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<serde_json::Value>;
|
|
|
|
/// Check compliance violations
|
|
async fn check_violations(&self, trade_id: &str) -> RiskDataResult<Vec<ComplianceEvent>>;
|
|
|
|
/// Mark event as reported to regulator
|
|
async fn mark_reported(&self, event_id: Uuid, reference: String) -> RiskDataResult<()>;
|
|
}
|
|
|
|
/// Compliance repository implementation
|
|
#[derive(Clone)]
|
|
pub struct ComplianceRepositoryImpl {
|
|
db_pool: PgPool,
|
|
redis_conn: ConnectionManager,
|
|
}
|
|
|
|
impl std::fmt::Debug for ComplianceRepositoryImpl {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ComplianceRepositoryImpl")
|
|
.field("db_pool", &"<PgPool>")
|
|
.field("redis_conn", &"<ConnectionManager>")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl ComplianceRepositoryImpl {
|
|
pub fn new(db_pool: PgPool, redis_conn: ConnectionManager) -> Self {
|
|
Self {
|
|
db_pool,
|
|
redis_conn,
|
|
}
|
|
}
|
|
|
|
/// Validate compliance event data
|
|
fn validate_event(&self, event: &ComplianceEvent) -> RiskDataResult<()> {
|
|
if event.description.is_empty() {
|
|
return Err(RiskDataError::ComplianceValidation(
|
|
"Event description cannot be empty".to_string(),
|
|
));
|
|
}
|
|
|
|
// Specific validations per event type
|
|
match event.event_type {
|
|
ComplianceEventType::TradeExecution | ComplianceEventType::OrderPlacement => {
|
|
if event.order_id.is_none() && event.trade_id.is_none() {
|
|
return Err(RiskDataError::ComplianceValidation(
|
|
"Trade/order events must have order_id or trade_id".to_string(),
|
|
));
|
|
}
|
|
}
|
|
ComplianceEventType::RiskBreach | ComplianceEventType::LimitExceeded => {
|
|
if event.severity != ComplianceSeverity::Critical
|
|
&& event.severity != ComplianceSeverity::Breach
|
|
{
|
|
return Err(RiskDataError::ComplianceValidation(
|
|
"Risk breaches must have Critical or Breach severity".to_string(),
|
|
));
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculate risk score for compliance event
|
|
fn calculate_risk_score(&self, event: &ComplianceEvent) -> Decimal {
|
|
let mut score = Decimal::ZERO;
|
|
|
|
// Base score by severity
|
|
score += match event.severity {
|
|
ComplianceSeverity::Info => Decimal::from(10),
|
|
ComplianceSeverity::Warning => Decimal::from(30),
|
|
ComplianceSeverity::Critical => Decimal::from(70),
|
|
ComplianceSeverity::Breach => Decimal::from(100),
|
|
};
|
|
|
|
// Additional score by event type
|
|
score += match event.event_type {
|
|
ComplianceEventType::RiskBreach | ComplianceEventType::LimitExceeded => {
|
|
Decimal::from(30)
|
|
}
|
|
ComplianceEventType::EmergencyAction => Decimal::from(25),
|
|
ComplianceEventType::ConfigurationChange => Decimal::from(20),
|
|
ComplianceEventType::BestExecutionCheck => Decimal::from(15),
|
|
_ => Decimal::from(5),
|
|
};
|
|
|
|
// Framework-specific adjustments
|
|
score += match event.framework {
|
|
RegulatoryFramework::Sox => Decimal::from(20),
|
|
RegulatoryFramework::MifidII => Decimal::from(15),
|
|
RegulatoryFramework::DoddFrank => Decimal::from(15),
|
|
_ => Decimal::from(5),
|
|
};
|
|
|
|
score.min(Decimal::from(100)) // Cap at 100
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ComplianceRepository for ComplianceRepositoryImpl {
|
|
async fn log_event(&self, mut event: ComplianceEvent) -> RiskDataResult<()> {
|
|
self.validate_event(&event)?;
|
|
|
|
// Calculate risk score if not provided
|
|
if event.risk_score.is_none() {
|
|
event.risk_score = Some(self.calculate_risk_score(&event));
|
|
}
|
|
|
|
let query = r#"
|
|
INSERT INTO compliance_events (
|
|
id, framework, event_type, severity, timestamp, user_id, session_id,
|
|
order_id, trade_id, symbol, quantity, price, venue, counterparty,
|
|
description, details, risk_score, remediation_required,
|
|
remediation_status, reported_to_regulator, regulator_reference
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
|
|
"#;
|
|
|
|
sqlx::query(query)
|
|
.bind(event.id)
|
|
.bind(event.framework)
|
|
.bind(event.event_type)
|
|
.bind(event.severity)
|
|
.bind(event.timestamp)
|
|
.bind(&event.user_id)
|
|
.bind(&event.session_id)
|
|
.bind(&event.order_id)
|
|
.bind(&event.trade_id)
|
|
.bind(&event.symbol)
|
|
.bind(event.quantity)
|
|
.bind(event.price)
|
|
.bind(&event.venue)
|
|
.bind(&event.counterparty)
|
|
.bind(&event.description)
|
|
.bind(&event.details)
|
|
.bind(event.risk_score)
|
|
.bind(event.remediation_required)
|
|
.bind(&event.remediation_status)
|
|
.bind(event.reported_to_regulator)
|
|
.bind(&event.regulator_reference)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
// Cache high-risk events in Redis for quick access
|
|
if let Some(risk_score) = event.risk_score {
|
|
if risk_score >= Decimal::from(70) {
|
|
let cache_key = format!("compliance:high_risk:{}", event.id);
|
|
let serialized = serde_json::to_string(&event)?;
|
|
|
|
let mut redis_conn = self.redis_conn.clone();
|
|
redis::cmd("SETEX")
|
|
.arg(&cache_key)
|
|
.arg(86400) // 24 hours TTL
|
|
.arg(&serialized)
|
|
.query_async::<()>(&mut redis_conn)
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
info!(
|
|
"Compliance event logged: {} - {} - {}",
|
|
event.framework as u8, event.event_type as u8, event.description
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_events(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
framework: Option<RegulatoryFramework>,
|
|
severity: Option<ComplianceSeverity>,
|
|
) -> RiskDataResult<Vec<ComplianceEvent>> {
|
|
let mut query =
|
|
"SELECT * FROM compliance_events WHERE timestamp BETWEEN $1 AND $2".to_string();
|
|
let mut bind_count = 2;
|
|
|
|
if framework.is_some() {
|
|
bind_count += 1;
|
|
query.push_str(&format!(" AND framework = ${}", bind_count));
|
|
}
|
|
|
|
if severity.is_some() {
|
|
bind_count += 1;
|
|
query.push_str(&format!(" AND severity = ${}", bind_count));
|
|
}
|
|
|
|
query.push_str(" ORDER BY timestamp DESC");
|
|
|
|
let mut db_query = sqlx::query_as::<_, ComplianceEvent>(&query)
|
|
.bind(from)
|
|
.bind(to);
|
|
|
|
if let Some(fw) = framework {
|
|
db_query = db_query.bind(fw);
|
|
}
|
|
|
|
if let Some(sev) = severity {
|
|
db_query = db_query.bind(sev);
|
|
}
|
|
|
|
let events = db_query.fetch_all(&self.db_pool).await?;
|
|
Ok(events)
|
|
}
|
|
|
|
async fn report_transaction(&self, report: TransactionReport) -> RiskDataResult<()> {
|
|
let query = r#"
|
|
INSERT INTO transaction_reports (
|
|
id, trade_id, instrument_id, isin, trading_date, trading_time,
|
|
quantity, price, currency, venue, counterparty_id,
|
|
investment_decision_within_firm, execution_decision_within_firm,
|
|
client_identification, short_selling_indicator,
|
|
commodity_derivative_indicator, securities_financing_transaction_indicator,
|
|
waiver_indicator, reported_at, report_status, arm_reference
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
|
|
ON CONFLICT (trade_id) DO UPDATE SET
|
|
report_status = EXCLUDED.report_status,
|
|
arm_reference = EXCLUDED.arm_reference
|
|
"#;
|
|
|
|
sqlx::query(query)
|
|
.bind(report.id)
|
|
.bind(&report.trade_id)
|
|
.bind(&report.instrument_id)
|
|
.bind(&report.isin)
|
|
.bind(report.trading_date)
|
|
.bind(report.trading_time)
|
|
.bind(report.quantity)
|
|
.bind(report.price)
|
|
.bind(&report.currency)
|
|
.bind(&report.venue)
|
|
.bind(&report.counterparty_id)
|
|
.bind(&report.investment_decision_within_firm)
|
|
.bind(&report.execution_decision_within_firm)
|
|
.bind(&report.client_identification)
|
|
.bind(report.short_selling_indicator)
|
|
.bind(&report.commodity_derivative_indicator)
|
|
.bind(report.securities_financing_transaction_indicator)
|
|
.bind(&report.waiver_indicator)
|
|
.bind(report.reported_at)
|
|
.bind(&report.report_status)
|
|
.bind(&report.arm_reference)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
info!("Transaction report submitted: {}", report.trade_id);
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_transaction_reports(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<TransactionReport>> {
|
|
let query = r#"
|
|
SELECT * FROM transaction_reports
|
|
WHERE trading_date BETWEEN $1 AND $2
|
|
ORDER BY trading_time DESC
|
|
"#;
|
|
|
|
let reports = sqlx::query_as::<_, TransactionReport>(query)
|
|
.bind(from)
|
|
.bind(to)
|
|
.fetch_all(&self.db_pool)
|
|
.await?;
|
|
|
|
Ok(reports)
|
|
}
|
|
|
|
async fn log_best_execution(&self, report: BestExecutionReport) -> RiskDataResult<()> {
|
|
let query = r#"
|
|
INSERT INTO best_execution_reports (
|
|
id, order_id, symbol, order_type, quantity, requested_price,
|
|
execution_price, execution_venue, execution_time, price_improvement,
|
|
execution_speed_ms, market_impact_bps, total_costs_bps,
|
|
alternative_venues, criteria_analysis, best_execution_achieved, justification
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
"#;
|
|
|
|
sqlx::query(query)
|
|
.bind(report.id)
|
|
.bind(&report.order_id)
|
|
.bind(&report.symbol)
|
|
.bind(&report.order_type)
|
|
.bind(report.quantity)
|
|
.bind(report.requested_price)
|
|
.bind(report.execution_price)
|
|
.bind(&report.execution_venue)
|
|
.bind(report.execution_time)
|
|
.bind(report.price_improvement)
|
|
.bind(report.execution_speed_ms)
|
|
.bind(report.market_impact_bps)
|
|
.bind(report.total_costs_bps)
|
|
.bind(&report.alternative_venues)
|
|
.bind(&report.criteria_analysis)
|
|
.bind(report.best_execution_achieved)
|
|
.bind(&report.justification)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
// Log compliance event if best execution was not achieved
|
|
if !report.best_execution_achieved {
|
|
let compliance_event = ComplianceEvent {
|
|
id: Uuid::new_v4(),
|
|
framework: RegulatoryFramework::MifidII,
|
|
event_type: ComplianceEventType::BestExecutionCheck,
|
|
severity: ComplianceSeverity::Warning,
|
|
timestamp: Utc::now(),
|
|
user_id: None,
|
|
session_id: None,
|
|
order_id: Some(report.order_id.clone()),
|
|
trade_id: None,
|
|
symbol: Some(report.symbol.clone()),
|
|
quantity: Some(report.quantity),
|
|
price: Some(report.execution_price),
|
|
venue: Some(report.execution_venue.clone()),
|
|
counterparty: None,
|
|
description: "Best execution not achieved".to_string(),
|
|
details: serde_json::json!({
|
|
"justification": report.justification,
|
|
"price_improvement": report.price_improvement,
|
|
"execution_speed_ms": report.execution_speed_ms
|
|
}),
|
|
risk_score: None,
|
|
remediation_required: true,
|
|
remediation_status: Some("pending".to_string()),
|
|
reported_to_regulator: false,
|
|
regulator_reference: None,
|
|
};
|
|
|
|
self.log_event(compliance_event).await?;
|
|
}
|
|
|
|
info!("Best execution report logged: {}", report.order_id);
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_best_execution_reports(
|
|
&self,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<BestExecutionReport>> {
|
|
let query = r#"
|
|
SELECT * FROM best_execution_reports
|
|
WHERE execution_time BETWEEN $1 AND $2
|
|
ORDER BY execution_time DESC
|
|
"#;
|
|
|
|
let reports = sqlx::query_as::<_, BestExecutionReport>(query)
|
|
.bind(from)
|
|
.bind(to)
|
|
.fetch_all(&self.db_pool)
|
|
.await?;
|
|
|
|
Ok(reports)
|
|
}
|
|
|
|
async fn create_audit_trail(&self, entry: AuditTrail) -> RiskDataResult<()> {
|
|
let query = r#"
|
|
INSERT INTO audit_trails (
|
|
id, timestamp, user_id, session_id, action, resource, resource_id,
|
|
ip_address, user_agent, success, error_message, before_state,
|
|
after_state, sensitive_data_accessed, retention_required_until
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
"#;
|
|
|
|
sqlx::query(query)
|
|
.bind(entry.id)
|
|
.bind(entry.timestamp)
|
|
.bind(&entry.user_id)
|
|
.bind(&entry.session_id)
|
|
.bind(&entry.action)
|
|
.bind(&entry.resource)
|
|
.bind(&entry.resource_id)
|
|
.bind(&entry.ip_address)
|
|
.bind(&entry.user_agent)
|
|
.bind(entry.success)
|
|
.bind(&entry.error_message)
|
|
.bind(&entry.before_state)
|
|
.bind(&entry.after_state)
|
|
.bind(entry.sensitive_data_accessed)
|
|
.bind(entry.retention_required_until)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
// Cache failed access attempts for security monitoring
|
|
if !entry.success {
|
|
let cache_key = format!("audit:failed:{}:{}", entry.user_id, entry.resource);
|
|
let mut redis_conn = self.redis_conn.clone();
|
|
|
|
redis::cmd("INCR")
|
|
.arg(&cache_key)
|
|
.query_async::<()>(&mut redis_conn)
|
|
.await?;
|
|
|
|
redis::cmd("EXPIRE")
|
|
.arg(&cache_key)
|
|
.arg(3600) // 1 hour window
|
|
.query_async::<()>(&mut redis_conn)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn search_audit_trail(
|
|
&self,
|
|
user_id: Option<String>,
|
|
action: Option<String>,
|
|
resource: Option<String>,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<Vec<AuditTrail>> {
|
|
let mut query = "SELECT * FROM audit_trails WHERE timestamp BETWEEN $1 AND $2".to_string();
|
|
let mut bind_count = 2;
|
|
|
|
if user_id.is_some() {
|
|
bind_count += 1;
|
|
query.push_str(&format!(" AND user_id = ${}", bind_count));
|
|
}
|
|
|
|
if action.is_some() {
|
|
bind_count += 1;
|
|
query.push_str(&format!(" AND action = ${}", bind_count));
|
|
}
|
|
|
|
if resource.is_some() {
|
|
bind_count += 1;
|
|
query.push_str(&format!(" AND resource = ${}", bind_count));
|
|
}
|
|
|
|
query.push_str(" ORDER BY timestamp DESC LIMIT 1000");
|
|
|
|
let mut db_query = sqlx::query_as::<_, AuditTrail>(&query).bind(from).bind(to);
|
|
|
|
if let Some(uid) = user_id {
|
|
db_query = db_query.bind(uid);
|
|
}
|
|
|
|
if let Some(act) = action {
|
|
db_query = db_query.bind(act);
|
|
}
|
|
|
|
if let Some(res) = resource {
|
|
db_query = db_query.bind(res);
|
|
}
|
|
|
|
let entries = db_query.fetch_all(&self.db_pool).await?;
|
|
Ok(entries)
|
|
}
|
|
|
|
async fn generate_regulatory_report(
|
|
&self,
|
|
framework: RegulatoryFramework,
|
|
from: DateTime<Utc>,
|
|
to: DateTime<Utc>,
|
|
) -> RiskDataResult<serde_json::Value> {
|
|
let events = self.get_events(from, to, Some(framework), None).await?;
|
|
|
|
let report = match framework {
|
|
RegulatoryFramework::Sox => {
|
|
serde_json::json!({
|
|
"framework": "SOX",
|
|
"period": {"from": from, "to": to},
|
|
"total_events": events.len(),
|
|
"critical_events": events.iter().filter(|e| e.severity == ComplianceSeverity::Critical).count(),
|
|
"breaches": events.iter().filter(|e| e.severity == ComplianceSeverity::Breach).count(),
|
|
"remediation_pending": events.iter().filter(|e| e.remediation_required && e.remediation_status.as_deref() == Some("pending")).count(),
|
|
"events": events
|
|
})
|
|
}
|
|
RegulatoryFramework::MifidII => {
|
|
let transaction_reports = self.get_transaction_reports(from, to).await?;
|
|
let best_execution_reports = self.get_best_execution_reports(from, to).await?;
|
|
|
|
serde_json::json!({
|
|
"framework": "MiFID II",
|
|
"period": {"from": from, "to": to},
|
|
"transaction_reports": transaction_reports.len(),
|
|
"best_execution_reports": best_execution_reports.len(),
|
|
"compliance_events": events.len(),
|
|
"transactions": transaction_reports,
|
|
"best_execution": best_execution_reports,
|
|
"events": events
|
|
})
|
|
}
|
|
_ => {
|
|
serde_json::json!({
|
|
"framework": format!("{:?}", framework),
|
|
"period": {"from": from, "to": to},
|
|
"total_events": events.len(),
|
|
"events": events
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(report)
|
|
}
|
|
|
|
async fn check_violations(&self, trade_id: &str) -> RiskDataResult<Vec<ComplianceEvent>> {
|
|
let query = r#"
|
|
SELECT * FROM compliance_events
|
|
WHERE trade_id = $1
|
|
AND severity IN ('critical', 'breach')
|
|
ORDER BY timestamp DESC
|
|
"#;
|
|
|
|
let violations = sqlx::query_as::<_, ComplianceEvent>(query)
|
|
.bind(trade_id)
|
|
.fetch_all(&self.db_pool)
|
|
.await?;
|
|
|
|
Ok(violations)
|
|
}
|
|
|
|
async fn mark_reported(&self, event_id: Uuid, reference: String) -> RiskDataResult<()> {
|
|
let query = r#"
|
|
UPDATE compliance_events
|
|
SET reported_to_regulator = true, regulator_reference = $2
|
|
WHERE id = $1
|
|
"#;
|
|
|
|
sqlx::query(query)
|
|
.bind(event_id)
|
|
.bind(&reference)
|
|
.execute(&self.db_pool)
|
|
.await?;
|
|
|
|
info!(
|
|
"Compliance event {} marked as reported with reference {}",
|
|
event_id, reference
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_compliance_event_validation() {
|
|
let repo = ComplianceRepositoryImpl {
|
|
db_pool: PgPool::connect("").unwrap_or_else(|_| panic!("Test DB connection")),
|
|
redis_conn: panic!("Mock Redis connection"),
|
|
};
|
|
|
|
let event = ComplianceEvent {
|
|
id: Uuid::new_v4(),
|
|
framework: RegulatoryFramework::Sox,
|
|
event_type: ComplianceEventType::TradeExecution,
|
|
severity: ComplianceSeverity::Info,
|
|
timestamp: Utc::now(),
|
|
user_id: None,
|
|
session_id: None,
|
|
order_id: None,
|
|
trade_id: None,
|
|
symbol: None,
|
|
quantity: None,
|
|
price: None,
|
|
venue: None,
|
|
counterparty: None,
|
|
description: "".to_string(), // Empty description should fail
|
|
details: serde_json::json!({}),
|
|
risk_score: None,
|
|
remediation_required: false,
|
|
remediation_status: None,
|
|
reported_to_regulator: false,
|
|
regulator_reference: None,
|
|
};
|
|
|
|
assert!(repo.validate_event(&event).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_risk_score_calculation() {
|
|
let repo = ComplianceRepositoryImpl {
|
|
db_pool: PgPool::connect("").unwrap_or_else(|_| panic!("Test DB connection")),
|
|
redis_conn: panic!("Mock Redis connection"),
|
|
};
|
|
|
|
let event = ComplianceEvent {
|
|
id: Uuid::new_v4(),
|
|
framework: RegulatoryFramework::Sox,
|
|
event_type: ComplianceEventType::RiskBreach,
|
|
severity: ComplianceSeverity::Breach,
|
|
timestamp: Utc::now(),
|
|
user_id: None,
|
|
session_id: None,
|
|
order_id: None,
|
|
trade_id: None,
|
|
symbol: None,
|
|
quantity: None,
|
|
price: None,
|
|
venue: None,
|
|
counterparty: None,
|
|
description: "Test risk breach".to_string(),
|
|
details: serde_json::json!({}),
|
|
risk_score: None,
|
|
remediation_required: true,
|
|
remediation_status: None,
|
|
reported_to_regulator: false,
|
|
regulator_reference: None,
|
|
};
|
|
|
|
let score = repo.calculate_risk_score(&event);
|
|
assert!(score > Decimal::ZERO);
|
|
assert!(score <= Decimal::from(100));
|
|
}
|
|
} |