sandbox_quant/lifecycle/
engine.rs1use 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}