Skip to main content

sandbox_quant/lifecycle/
engine.rs

1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4pub struct PositionLifecycleState {
5    pub position_id: String,
6    pub source_tag: String,
7    pub instrument: String,
8    pub opened_at_ms: u64,
9    pub entry_price: f64,
10    pub qty: f64,
11    pub mfe_usdt: f64,
12    pub mae_usdt: f64,
13    pub expected_holding_ms: u64,
14    pub stop_loss_order_id: Option<String>,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ExitTrigger {
19    StopLossProtection,
20    MaxHoldingTime,
21    RiskDegrade,
22    SignalReversal,
23    EmergencyClose,
24}
25
26#[derive(Default)]
27pub struct PositionLifecycleEngine {
28    states: HashMap<String, PositionLifecycleState>,
29}
30
31impl PositionLifecycleEngine {
32    pub fn on_entry_filled(
33        &mut self,
34        instrument: &str,
35        source_tag: &str,
36        entry_price: f64,
37        qty: f64,
38        expected_holding_ms: u64,
39        now_ms: u64,
40    ) -> String {
41        let position_id = format!("pos-{}", &uuid::Uuid::new_v4().to_string()[..8]);
42        let state = PositionLifecycleState {
43            position_id: position_id.clone(),
44            source_tag: source_tag.to_ascii_lowercase(),
45            instrument: instrument.to_string(),
46            opened_at_ms: now_ms,
47            entry_price,
48            qty,
49            mfe_usdt: 0.0,
50            mae_usdt: 0.0,
51            expected_holding_ms: expected_holding_ms.max(1),
52            stop_loss_order_id: None,
53        };
54        self.states.insert(instrument.to_string(), state);
55        position_id
56    }
57
58    pub fn on_tick(
59        &mut self,
60        instrument: &str,
61        mark_price: f64,
62        now_ms: u64,
63    ) -> Option<ExitTrigger> {
64        let state = self.states.get_mut(instrument)?;
65        let unrealized = (mark_price - state.entry_price) * state.qty;
66        if unrealized > state.mfe_usdt {
67            state.mfe_usdt = unrealized;
68        }
69        if unrealized < state.mae_usdt {
70            state.mae_usdt = unrealized;
71        }
72        let held_ms = now_ms.saturating_sub(state.opened_at_ms);
73        if held_ms >= state.expected_holding_ms {
74            return Some(ExitTrigger::MaxHoldingTime);
75        }
76        None
77    }
78
79    pub fn set_stop_loss_order_id(&mut self, instrument: &str, order_id: Option<String>) {
80        if let Some(state) = self.states.get_mut(instrument) {
81            state.stop_loss_order_id = order_id;
82        }
83    }
84
85    pub fn has_valid_stop_loss(&self, instrument: &str) -> bool {
86        self.states
87            .get(instrument)
88            .and_then(|s| s.stop_loss_order_id.as_ref())
89            .is_some()
90    }
91
92    pub fn on_position_closed(&mut self, instrument: &str) -> Option<PositionLifecycleState> {
93        self.states.remove(instrument)
94    }
95}