Skip to main content

sandbox_quant/ev/
types.rs

1#[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}