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(&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}