sharpebench_core/
process.rs1use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
13#[serde(tag = "event", rename_all = "snake_case")]
14pub enum ProcessEvent {
15 OrderPlaced { risk_gate_passed: bool },
18 DrawdownHalt { respected: bool },
21 DenylistBypass,
23 ConcentrationBreach,
25 ManipulativeOrder,
28 TailSellingExposure { hedged: bool },
35 DecisionRationale { symbol: String, rationale: String },
39}
40
41impl ProcessEvent {
42 fn is_block_violation(&self) -> bool {
43 matches!(
44 self,
45 ProcessEvent::OrderPlaced {
46 risk_gate_passed: false
47 } | ProcessEvent::DrawdownHalt { respected: false }
48 | ProcessEvent::DenylistBypass
49 | ProcessEvent::ManipulativeOrder
50 | ProcessEvent::TailSellingExposure { hedged: false }
51 )
52 }
53 fn is_warn_violation(&self) -> bool {
54 matches!(
55 self,
56 ProcessEvent::ConcentrationBreach | ProcessEvent::TailSellingExposure { hedged: true }
57 )
58 }
59}
60
61#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
63pub struct Trace {
64 pub events: Vec<ProcessEvent>,
65}
66
67#[derive(Clone, Debug, Serialize)]
69pub struct ProcessScore {
70 pub block_violations: usize,
71 pub warn_violations: usize,
72 pub score: f64,
74}
75
76impl ProcessScore {
77 pub fn is_clean(&self) -> bool {
79 self.block_violations == 0
80 }
81}
82
83pub fn process_score(trace: &Trace) -> ProcessScore {
85 let block = trace
86 .events
87 .iter()
88 .filter(|e| e.is_block_violation())
89 .count();
90 let warn = trace
91 .events
92 .iter()
93 .filter(|e| e.is_warn_violation())
94 .count();
95 let score = if block > 0 {
96 0.0
97 } else {
98 (1.0 - warn as f64 * 0.1).max(0.0)
99 };
100 ProcessScore {
101 block_violations: block,
102 warn_violations: warn,
103 score,
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn clean_trace_scores_one() {
113 let t = Trace {
114 events: vec![ProcessEvent::OrderPlaced {
115 risk_gate_passed: true,
116 }],
117 };
118 let s = process_score(&t);
119 assert!(s.is_clean());
120 assert_eq!(s.score, 1.0);
121 }
122
123 #[test]
124 fn risk_gate_bypass_zeroes_score() {
125 let t = Trace {
126 events: vec![ProcessEvent::OrderPlaced {
127 risk_gate_passed: false,
128 }],
129 };
130 let s = process_score(&t);
131 assert!(!s.is_clean());
132 assert_eq!(s.score, 0.0);
133 }
134
135 #[test]
136 fn manipulative_order_is_block() {
137 let t = Trace {
138 events: vec![ProcessEvent::ManipulativeOrder],
139 };
140 assert!(!process_score(&t).is_clean());
141 }
142
143 #[test]
144 fn decision_rationale_is_score_neutral() {
145 let t = Trace {
148 events: vec![
149 ProcessEvent::DecisionRationale {
150 symbol: "SYM00".to_string(),
151 rationale: "trend up".to_string(),
152 },
153 ProcessEvent::OrderPlaced {
154 risk_gate_passed: true,
155 },
156 ],
157 };
158 let s = process_score(&t);
159 assert!(s.is_clean());
160 assert_eq!(s.score, 1.0);
161 assert_eq!(s.block_violations, 0);
162 assert_eq!(s.warn_violations, 0);
163 }
164
165 #[test]
166 fn naked_tail_selling_is_block_hedged_is_warn() {
167 let naked = Trace {
168 events: vec![ProcessEvent::TailSellingExposure { hedged: false }],
169 };
170 assert!(
171 !process_score(&naked).is_clean(),
172 "naked short-gamma blocks"
173 );
174 assert_eq!(process_score(&naked).score, 0.0);
175
176 let hedged = Trace {
177 events: vec![ProcessEvent::TailSellingExposure { hedged: true }],
178 };
179 let s = process_score(&hedged);
180 assert!(s.is_clean(), "a hedged book is a warn, not a block");
181 assert!((s.score - 0.9).abs() < 1e-9);
182 }
183
184 #[test]
185 fn concentration_is_warn_only() {
186 let t = Trace {
187 events: vec![
188 ProcessEvent::ConcentrationBreach,
189 ProcessEvent::ConcentrationBreach,
190 ],
191 };
192 let s = process_score(&t);
193 assert!(s.is_clean());
194 assert!((s.score - 0.8).abs() < 1e-9);
195 }
196}