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(&self, recency_lambda: f64, loss_threshold_usdt: f64) -> (f64, f64) {
64 let mut tail_events = 0.0;
65 let mut loss_events = 0.0;
66 for s in &self.samples {
67 if s.pnl_usdt >= 0.0 {
68 continue;
69 }
70 let w = recency_weight(s.age_days, recency_lambda);
71 loss_events += w;
72 if s.pnl_usdt <= -loss_threshold_usdt {
73 tail_events += w;
74 }
75 }
76 (tail_events, loss_events)
77 }
78
79 pub fn weighted_avg_win_loss(&self, recency_lambda: f64) -> (f64, f64) {
80 let mut win_sum = 0.0;
81 let mut win_w = 0.0;
82 let mut loss_sum = 0.0;
83 let mut loss_w = 0.0;
84 for s in &self.samples {
85 let w = recency_weight(s.age_days, recency_lambda);
86 if s.pnl_usdt > 0.0 {
87 win_sum += s.pnl_usdt * w;
88 win_w += w;
89 } else if s.pnl_usdt < 0.0 {
90 loss_sum += s.pnl_usdt.abs() * w;
91 loss_w += w;
92 }
93 }
94 let avg_win = if win_w > f64::EPSILON {
95 win_sum / win_w
96 } else {
97 0.0
98 };
99 let avg_loss = if loss_w > f64::EPSILON {
100 loss_sum / loss_w
101 } else {
102 0.0
103 };
104 (avg_win, avg_loss)
105 }
106
107 pub fn median_holding_ms(&self) -> u64 {
108 if self.samples.is_empty() {
109 return 0;
110 }
111 let mut values: Vec<u64> = self.samples.iter().map(|s| s.holding_ms).collect();
112 values.sort_unstable();
113 values[values.len() / 2]
114 }
115
116 pub fn q05_loss_abs_usdt(&self) -> f64 {
117 let mut losses: Vec<f64> = self
118 .samples
119 .iter()
120 .filter_map(|s| (s.pnl_usdt < 0.0).then_some(s.pnl_usdt.abs()))
121 .collect();
122 if losses.is_empty() {
123 return 0.0;
124 }
125 losses.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
126 let idx = ((losses.len() as f64) * 0.05).floor() as usize;
127 losses[idx.min(losses.len() - 1)]
128 }
129}
130
131pub fn recency_weight(age_days: f64, lambda: f64) -> f64 {
132 (-lambda.max(0.0) * age_days.max(0.0)).exp()
133}