Skip to main content

rustrade_execution/order/
id.rs

1use derive_more::{Display, From};
2use rand::prelude::IndexedRandom;
3use serde::{Deserialize, Serialize};
4use smol_str::SmolStr;
5
6#[derive(
7    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
8)]
9pub struct ClientOrderId<T = SmolStr>(pub T);
10
11impl ClientOrderId<SmolStr> {
12    /// Construct a `ClientOrderId` from the specified string.
13    ///
14    /// Use [`Self::random`] to generate a random stack-allocated `ClientOrderId`.
15    pub fn new<S: Into<SmolStr>>(id: S) -> Self {
16        Self(id.into())
17    }
18
19    /// Construct a `ClientOrderId` containing a UUID v4 string.
20    ///
21    /// Produces lowercase hyphenated format (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, 36 chars),
22    /// the format required by Hyperliquid's `cancel_by_cloid()` endpoint.
23    ///
24    /// Required for Hyperliquid trigger orders (Stop, TakeProfit, etc.), which must use
25    /// UUID-format client order IDs for `cancel_by_cloid()` to work. Regular orders can
26    /// use [`Self::random`] for better performance (stack-allocated, no heap).
27    #[cfg(feature = "hyperliquid")]
28    pub fn uuid() -> Self {
29        Self(SmolStr::new(uuid::Uuid::new_v4().to_string()))
30    }
31
32    /// Construct a stack-allocated `ClientOrderId` backed by a 23 byte [`SmolStr`].
33    pub fn random() -> Self {
34        const LEN_URL_SAFE_SYMBOLS: usize = 64;
35        const URL_SAFE_SYMBOLS: [char; LEN_URL_SAFE_SYMBOLS] = [
36            '_', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
37            'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
38            'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
39            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
40        ];
41        // SmolStr can be up to 23 bytes long without allocating
42        const LEN_NON_ALLOCATING_CID: usize = 23;
43
44        let mut thread_rng = rand::rng();
45
46        #[allow(clippy::expect_used)] // Invariant: URL_SAFE_SYMBOLS is a const 64-element array
47        let random_utf8: [u8; LEN_NON_ALLOCATING_CID] = std::array::from_fn(|_| {
48            let symbol = URL_SAFE_SYMBOLS
49                .choose(&mut thread_rng)
50                .expect("URL_SAFE_SYMBOLS slice is not empty");
51
52            *symbol as u8
53        });
54
55        #[allow(clippy::expect_used)] // Invariant: all URL_SAFE_SYMBOLS chars are ASCII
56        let random_utf8_str =
57            std::str::from_utf8(&random_utf8).expect("URL_SAFE_SYMBOLS are valid utf8");
58
59        Self(SmolStr::new_inline(random_utf8_str))
60    }
61}
62
63impl Default for ClientOrderId<SmolStr> {
64    fn default() -> Self {
65        Self::random()
66    }
67}
68
69#[derive(
70    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
71)]
72pub struct OrderId<T = SmolStr>(pub T);
73
74impl OrderId {
75    pub fn new<S: AsRef<str>>(id: S) -> Self {
76        Self(SmolStr::new(id))
77    }
78}
79
80#[derive(
81    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
82)]
83pub struct StrategyId(pub SmolStr);
84
85impl StrategyId {
86    pub fn new<S: AsRef<str>>(id: S) -> Self {
87        Self(SmolStr::new(id))
88    }
89
90    pub fn unknown() -> Self {
91        // "unknown" is 7 bytes — always inline; new_inline avoids the heap-allocation
92        // branch in SmolStr::new and is safe because the literal fits.
93        Self(SmolStr::new_inline("unknown"))
94    }
95
96    /// The fixed `StrategyId` used for synthetic settlement trades generated by the engine
97    /// on `ContractExpiry`. Using a `pub const` prevents bypassing any future validation.
98    ///
99    /// Note: conceptually this constant belongs to `rustrade::engine` (it is used exclusively
100    /// by the engine's contract lifecycle logic). It lives here because `StrategyId` is
101    /// defined in `rustrade-execution`; moving it to `rustrade::engine` would add a `smol_str`
102    /// import to that crate for a single constant.
103    pub const ENGINE_EXPIRY: StrategyId = StrategyId(SmolStr::new_static("__engine_expiry__"));
104}
105
106/// Opaque identifier for a tracked position.
107///
108/// In `OmsMode::Netting` this is always `"netting"` (at most one position per instrument).
109/// In `OmsMode::Hedging` this is derived from the `ClientOrderId` of the opening order,
110/// specified via [`crate::order::request::RequestOpen::position_id`] at order submission.
111#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
112pub struct PositionId(pub SmolStr);
113
114impl PositionId {
115    pub fn new(id: impl Into<SmolStr>) -> Self {
116        Self(id.into())
117    }
118
119    /// The fixed `PositionId` used for all netting-mode positions.
120    pub const NETTING: PositionId = PositionId(SmolStr::new_static("netting"));
121}
122
123impl Default for PositionId {
124    fn default() -> Self {
125        Self::NETTING
126    }
127}
128
129impl std::fmt::Display for PositionId {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        self.0.fmt(f)
132    }
133}
134
135#[cfg(test)]
136#[allow(clippy::expect_used)] // Test code: panics on bad input are acceptable
137mod tests {
138    use super::*;
139
140    #[test]
141    fn client_order_id_random_is_23_bytes() {
142        let cid = ClientOrderId::random();
143        assert_eq!(cid.0.len(), 23);
144    }
145
146    #[test]
147    #[cfg(feature = "hyperliquid")]
148    fn client_order_id_uuid_is_valid_uuid() {
149        let cid = ClientOrderId::uuid();
150        // UUID v4 string is 36 chars (8-4-4-4-12 with hyphens)
151        assert_eq!(cid.0.len(), 36);
152        // Verify it parses as a valid UUID
153        uuid::Uuid::parse_str(&cid.0).expect("should be valid UUID");
154    }
155}