use derive_more::{Display, From};
use rand::prelude::IndexedRandom;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
)]
pub struct ClientOrderId<T = SmolStr>(pub T);
impl ClientOrderId<SmolStr> {
pub fn new<S: Into<SmolStr>>(id: S) -> Self {
Self(id.into())
}
#[cfg(feature = "hyperliquid")]
pub fn uuid() -> Self {
Self(SmolStr::new(uuid::Uuid::new_v4().to_string()))
}
pub fn random() -> Self {
const LEN_URL_SAFE_SYMBOLS: usize = 64;
const URL_SAFE_SYMBOLS: [char; LEN_URL_SAFE_SYMBOLS] = [
'_', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
const LEN_NON_ALLOCATING_CID: usize = 23;
let mut thread_rng = rand::rng();
#[allow(clippy::expect_used)] let random_utf8: [u8; LEN_NON_ALLOCATING_CID] = std::array::from_fn(|_| {
let symbol = URL_SAFE_SYMBOLS
.choose(&mut thread_rng)
.expect("URL_SAFE_SYMBOLS slice is not empty");
*symbol as u8
});
#[allow(clippy::expect_used)] let random_utf8_str =
std::str::from_utf8(&random_utf8).expect("URL_SAFE_SYMBOLS are valid utf8");
Self(SmolStr::new_inline(random_utf8_str))
}
}
impl Default for ClientOrderId<SmolStr> {
fn default() -> Self {
Self::random()
}
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
)]
pub struct OrderId<T = SmolStr>(pub T);
impl OrderId {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Self(SmolStr::new(id))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
pub enum VenueOrderId {
Assigned(OrderId),
ClientAssigned,
}
impl VenueOrderId {
pub fn assigned(&self) -> Option<&OrderId> {
match self {
Self::Assigned(id) => Some(id),
Self::ClientAssigned => None,
}
}
pub fn is_same_order_as(&self, other: &Self) -> bool {
match (self.assigned(), other.assigned()) {
(Some(lhs), Some(rhs)) => lhs == rhs,
_ => false,
}
}
pub fn contradicts(&self, other: &Self) -> bool {
match (self.assigned(), other.assigned()) {
(Some(lhs), Some(rhs)) => lhs != rhs,
_ => false,
}
}
pub fn or_client_id(&self, cid: &ClientOrderId) -> OrderId {
match self {
Self::Assigned(id) => id.clone(),
Self::ClientAssigned => OrderId(cid.0.clone()),
}
}
}
impl From<OrderId> for VenueOrderId {
fn from(id: OrderId) -> Self {
Self::Assigned(id)
}
}
impl std::fmt::Display for VenueOrderId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Assigned(id) => id.fmt(f),
Self::ClientAssigned => f.write_str("<client-assigned>"),
}
}
}
#[derive(
Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display, From,
)]
pub struct StrategyId(pub SmolStr);
impl StrategyId {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Self(SmolStr::new(id))
}
pub fn unknown() -> Self {
Self(SmolStr::new_inline("unknown"))
}
pub const ENGINE_EXPIRY: StrategyId = StrategyId(SmolStr::new_static("__engine_expiry__"));
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct PositionId(pub SmolStr);
impl PositionId {
pub fn new(id: impl Into<SmolStr>) -> Self {
Self(id.into())
}
pub const NETTING: PositionId = PositionId(SmolStr::new_static("netting"));
}
impl Default for PositionId {
fn default() -> Self {
Self::NETTING
}
}
impl std::fmt::Display for PositionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
#[test]
fn client_order_id_random_is_23_bytes() {
let cid = ClientOrderId::random();
assert_eq!(cid.0.len(), 23);
}
#[test]
fn assigned_exposes_only_an_id_the_venue_gave() {
assert_eq!(
VenueOrderId::Assigned(OrderId::new("12345")).assigned(),
Some(&OrderId::new("12345"))
);
assert_eq!(VenueOrderId::ClientAssigned.assigned(), None);
}
#[test]
fn two_orders_the_venue_has_not_named_are_not_shown_to_be_the_same_order() {
let unnamed = VenueOrderId::ClientAssigned;
let also_unnamed = VenueOrderId::ClientAssigned;
assert_eq!(unnamed, also_unnamed, "the derived equality still holds");
assert!(!unnamed.is_same_order_as(&also_unnamed));
assert!(!unnamed.contradicts(&also_unnamed));
}
#[test]
fn identity_needs_two_agreeing_venue_ids_and_contradiction_needs_two_disagreeing_ones() {
let a = VenueOrderId::Assigned(OrderId::new("A"));
let a_again = VenueOrderId::Assigned(OrderId::new("A"));
let b = VenueOrderId::Assigned(OrderId::new("B"));
let unnamed = VenueOrderId::ClientAssigned;
assert!(a.is_same_order_as(&a_again));
assert!(!a.contradicts(&a_again));
assert!(!a.is_same_order_as(&b));
assert!(a.contradicts(&b));
assert!(!a.is_same_order_as(&unnamed));
assert!(!a.contradicts(&unnamed));
assert!(!unnamed.is_same_order_as(&a));
assert!(!unnamed.contradicts(&a));
}
#[test]
fn a_terminal_record_falls_back_to_the_client_id() {
let cid = ClientOrderId::new("cid-1");
assert_eq!(
VenueOrderId::Assigned(OrderId::new("12345")).or_client_id(&cid),
OrderId::new("12345")
);
assert_eq!(
VenueOrderId::ClientAssigned.or_client_id(&cid),
OrderId::new("cid-1")
);
}
#[test]
fn display_names_an_unnamed_order_rather_than_rendering_nothing() {
assert_eq!(
VenueOrderId::Assigned(OrderId::new("12345")).to_string(),
"12345"
);
assert_eq!(
VenueOrderId::ClientAssigned.to_string(),
"<client-assigned>"
);
}
#[test]
#[cfg(feature = "hyperliquid")]
fn client_order_id_uuid_is_valid_uuid() {
let cid = ClientOrderId::uuid();
assert_eq!(cid.0.len(), 36);
uuid::Uuid::parse_str(&cid.0).expect("should be valid UUID");
}
}