use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum ProcessEvent {
OrderPlaced { risk_gate_passed: bool },
DrawdownHalt { respected: bool },
DenylistBypass,
ConcentrationBreach,
ManipulativeOrder,
TailSellingExposure { hedged: bool },
DecisionRationale { symbol: String, rationale: String },
}
impl ProcessEvent {
fn is_block_violation(&self) -> bool {
matches!(
self,
ProcessEvent::OrderPlaced {
risk_gate_passed: false
} | ProcessEvent::DrawdownHalt { respected: false }
| ProcessEvent::DenylistBypass
| ProcessEvent::ManipulativeOrder
| ProcessEvent::TailSellingExposure { hedged: false }
)
}
fn is_warn_violation(&self) -> bool {
matches!(
self,
ProcessEvent::ConcentrationBreach | ProcessEvent::TailSellingExposure { hedged: true }
)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Trace {
pub events: Vec<ProcessEvent>,
}
#[derive(Clone, Debug, Serialize)]
pub struct ProcessScore {
pub block_violations: usize,
pub warn_violations: usize,
pub score: f64,
}
impl ProcessScore {
pub fn is_clean(&self) -> bool {
self.block_violations == 0
}
}
pub fn process_score(trace: &Trace) -> ProcessScore {
let block = trace
.events
.iter()
.filter(|e| e.is_block_violation())
.count();
let warn = trace
.events
.iter()
.filter(|e| e.is_warn_violation())
.count();
let score = if block > 0 {
0.0
} else {
(1.0 - warn as f64 * 0.1).max(0.0)
};
ProcessScore {
block_violations: block,
warn_violations: warn,
score,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_trace_scores_one() {
let t = Trace {
events: vec![ProcessEvent::OrderPlaced {
risk_gate_passed: true,
}],
};
let s = process_score(&t);
assert!(s.is_clean());
assert_eq!(s.score, 1.0);
}
#[test]
fn risk_gate_bypass_zeroes_score() {
let t = Trace {
events: vec![ProcessEvent::OrderPlaced {
risk_gate_passed: false,
}],
};
let s = process_score(&t);
assert!(!s.is_clean());
assert_eq!(s.score, 0.0);
}
#[test]
fn manipulative_order_is_block() {
let t = Trace {
events: vec![ProcessEvent::ManipulativeOrder],
};
assert!(!process_score(&t).is_clean());
}
#[test]
fn decision_rationale_is_score_neutral() {
let t = Trace {
events: vec![
ProcessEvent::DecisionRationale {
symbol: "SYM00".to_string(),
rationale: "trend up".to_string(),
},
ProcessEvent::OrderPlaced {
risk_gate_passed: true,
},
],
};
let s = process_score(&t);
assert!(s.is_clean());
assert_eq!(s.score, 1.0);
assert_eq!(s.block_violations, 0);
assert_eq!(s.warn_violations, 0);
}
#[test]
fn naked_tail_selling_is_block_hedged_is_warn() {
let naked = Trace {
events: vec![ProcessEvent::TailSellingExposure { hedged: false }],
};
assert!(
!process_score(&naked).is_clean(),
"naked short-gamma blocks"
);
assert_eq!(process_score(&naked).score, 0.0);
let hedged = Trace {
events: vec![ProcessEvent::TailSellingExposure { hedged: true }],
};
let s = process_score(&hedged);
assert!(s.is_clean(), "a hedged book is a warn, not a block");
assert!((s.score - 0.9).abs() < 1e-9);
}
#[test]
fn concentration_is_warn_only() {
let t = Trace {
events: vec![
ProcessEvent::ConcentrationBreach,
ProcessEvent::ConcentrationBreach,
],
};
let s = process_score(&t);
assert!(s.is_clean());
assert!((s.score - 0.8).abs() < 1e-9);
}
}