Skip to main content

sandbox_quant/lifecycle/
engine.rs

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