sandbox_quant/ev/
types.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2pub enum ConfidenceLevel {
3 Low,
4 Medium,
5 High,
6}
7
8#[derive(Debug, Clone)]
9pub struct ProbabilitySnapshot {
10 pub p_win: f64,
11 pub p_tail_loss: f64,
12 pub p_timeout_exit: f64,
13 pub n_eff: f64,
14 pub confidence: ConfidenceLevel,
15 pub prob_model_version: String,
16}
17
18#[derive(Debug, Clone)]
19pub struct EntryExpectancySnapshot {
20 pub expected_return_usdt: f64,
21 pub expected_holding_ms: u64,
22 pub worst_case_loss_usdt: f64,
23 pub fee_slippage_penalty_usdt: f64,
24 pub probability: ProbabilitySnapshot,
25 pub ev_model_version: String,
26 pub computed_at_ms: u64,
27}
28
29#[derive(Debug, Clone)]
30pub struct TradeStatsSample {
31 pub age_days: f64,
32 pub pnl_usdt: f64,
33 pub holding_ms: u64,
34}
35
36#[derive(Debug, Clone, Default)]
37pub struct TradeStatsWindow {
38 pub samples: Vec<TradeStatsSample>,
39}
40
41impl TradeStatsWindow {
42 pub fn n_eff(&self, recency_lambda: f64) -> f64 {
43 self.samples
44 .iter()
45 .map(|s| recency_weight(s.age_days, recency_lambda))
46 .sum()
47 }
48
49 pub fn weighted_win_loss(&self, recency_lambda: f64) -> (f64, f64) {
50 let mut wins = 0.0;
51 let mut losses = 0.0;
52 for s in &self.samples {
53 let w = recency_weight(s.age_days, recency_lambda);
54 if s.pnl_usdt > 0.0 {
55 wins += w;
56 } else if s.pnl_usdt < 0.0 {
57 losses += w;
58 }
59 }
60 (wins, losses)
61 }
62
63 pub fn weighted_tail_events(
64 &self,
65 recency_lambda: f64,
66 loss_threshold_usdt: f64,
67 ) -> (f64, f64) {
68 let mut tail_events = 0.0;
69 let mut loss_events = 0.0;
70 for s in &self.samples {
71 if s.pnl_usdt >= 0.0 {
72 continue;
73 }
74 let w = recency_weight(s.age_days, recency_lambda);
75 loss_events += w;
76 if s.pnl_usdt <= -loss_threshold_usdt {
77 tail_events += w;
78 }
79 }
80 (tail_events, loss_events)
81 }
82
83 pub fn weighted_avg_win_loss(&self, recency_lambda: f64) -> (f64, f64) {
84 let mut win_sum = 0.0;
85 let mut win_w = 0.0;
86 let mut loss_sum = 0.0;
87 let mut loss_w = 0.0;
88 for s in &self.samples {
89 let w = recency_weight(s.age_days, recency_lambda);
90 if s.pnl_usdt > 0.0 {
91 win_sum += s.pnl_usdt * w;
92 win_w += w;
93 } else if s.pnl_usdt < 0.0 {
94 loss_sum += s.pnl_usdt.abs() * w;
95 loss_w += w;
96 }
97 }
98 let avg_win = if win_w > f64::EPSILON {
99 win_sum / win_w
100 } else {
101 0.0
102 };
103 let avg_loss = if loss_w > f64::EPSILON {
104 loss_sum / loss_w
105 } else {
106 0.0
107 };
108 (avg_win, avg_loss)
109 }
110
111 pub fn median_holding_ms(&self) -> u64 {
112 if self.samples.is_empty() {
113 return 0;
114 }
115 let mut values: Vec<u64> = self.samples.iter().map(|s| s.holding_ms).collect();
116 values.sort_unstable();
117 values[values.len() / 2]
118 }
119
120 pub fn q05_loss_abs_usdt(&self) -> f64 {
121 let mut losses: Vec<f64> = self
122 .samples
123 .iter()
124 .filter_map(|s| (s.pnl_usdt < 0.0).then_some(s.pnl_usdt.abs()))
125 .collect();
126 if losses.is_empty() {
127 return 0.0;
128 }
129 losses.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
130 let idx = ((losses.len() as f64) * 0.05).floor() as usize;
131 losses[idx.min(losses.len() - 1)]
132 }
133}
134
135pub fn recency_weight(age_days: f64, lambda: f64) -> f64 {
136 (-lambda.max(0.0) * age_days.max(0.0)).exp()
137}