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