use serde::{Deserialize, Serialize};
use crate::types::{Cloid, MarketId, OrderId};
use crate::wallet::Address;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Side {
Bid,
Ask,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderKind {
Limit,
Market,
StopLoss,
TakeProfit,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TimeInForce {
Gtc,
Ioc,
Aon,
Alo,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PositionSide {
Long,
Short,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StpMode {
CancelOldest,
CancelNewest,
CancelBoth,
Reject,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Order {
pub owner: Address,
pub market: MarketId,
pub side: Side,
pub kind: OrderKind,
pub size: u64,
pub limit_px: u64,
pub tif: TimeInForce,
pub stp_mode: StpMode,
pub reduce_only: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloid: Option<Cloid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub builder: Option<Builder>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position_side: Option<PositionSide>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger: Option<Trigger>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TpSl {
Tp,
Sl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Trigger {
pub trigger_px: u64,
pub is_market: bool,
pub tpsl: TpSl,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Builder {
pub fee: u16,
pub user: Address,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CancelOrder {
pub owner: Address,
pub market: MarketId,
#[serde(skip_serializing_if = "Option::is_none")]
pub oid: Option<OrderId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloid: Option<Cloid>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Modify {
pub market: MarketId,
pub oid: OrderId,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_px: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_size: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct BatchModify {
pub modifications: Vec<Modify>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderGrouping {
#[default]
Na,
NormalTpsl,
PositionTpsl,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct BatchOrder {
pub orders: Vec<Order>,
#[serde(default)]
pub grouping: OrderGrouping,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct BatchCancel {
pub cancels: Vec<CancelOrder>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CancelByCloid {
pub asset: MarketId,
pub cloid: Cloid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct ScheduleCancel {
pub cancel_at_block: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CancelAllOrders {
#[serde(skip_serializing_if = "Option::is_none")]
pub asset: Option<MarketId>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrderStatus {
Resting(RestingStatus),
Filled(FilledStatus),
Error(String),
}
impl OrderStatus {
#[must_use]
pub fn oid(&self) -> Option<OrderId> {
match self {
OrderStatus::Resting(r) => Some(r.oid),
OrderStatus::Filled(f) => Some(f.oid),
OrderStatus::Error(_) => None,
}
}
#[must_use]
pub const fn is_error(&self) -> bool {
matches!(self, OrderStatus::Error(_))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct RestingStatus {
pub oid: OrderId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cloid: Option<Cloid>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FilledStatus {
pub total_sz: String,
pub avg_px: String,
pub oid: OrderId,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrderResponse {
pub statuses: Vec<OrderStatus>,
}
impl OrderResponse {
#[must_use]
pub fn first(&self) -> Option<&OrderStatus> {
self.statuses.first()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_order() -> Order {
Order {
owner: Address::ZERO,
market: MarketId(1),
side: Side::Bid,
kind: OrderKind::Limit,
size: 1_000,
limit_px: 5_000_000_000_000,
tif: TimeInForce::Gtc,
stp_mode: StpMode::CancelOldest,
reduce_only: false,
cloid: None,
builder: None,
position_side: None,
trigger: None,
}
}
#[test]
fn order_serializes_snake_case() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(j.get("limit_px").is_some(), "expected snake_case field");
assert!(j.get("reduce_only").is_some());
assert!(j.get("stp_mode").is_some());
assert!(j.get("limitPx").is_none());
assert!(j.get("reduceOnly").is_none());
assert!(j.get("stpMode").is_none());
}
#[test]
fn side_serializes_snake_case() {
assert_eq!(serde_json::to_string(&Side::Bid).unwrap(), "\"bid\"");
assert_eq!(serde_json::to_string(&Side::Ask).unwrap(), "\"ask\"");
}
#[test]
fn tif_serializes_snake_case() {
for (tif, expected) in [
(TimeInForce::Gtc, "\"gtc\""),
(TimeInForce::Ioc, "\"ioc\""),
(TimeInForce::Aon, "\"aon\""),
(TimeInForce::Alo, "\"alo\""),
] {
assert_eq!(serde_json::to_string(&tif).unwrap(), expected);
}
}
#[test]
fn order_serializes_size_as_integer_not_string() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(j["size"].is_number(), "size must be a plain JSON number");
assert!(
j["limit_px"].is_number(),
"limit_px must be a plain JSON number"
);
}
#[test]
fn order_round_trips() {
let o = sample_order();
let j = serde_json::to_string(&o).unwrap();
let dec: Order = serde_json::from_str(&j).unwrap();
assert_eq!(o, dec);
}
#[test]
fn order_omits_none_cloid() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(j.get("cloid").is_none());
}
#[test]
fn order_omits_none_builder() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(j.get("builder").is_none());
}
#[test]
fn order_omits_none_position_side() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(j.get("position_side").is_none());
}
#[test]
fn one_way_order_bytes_unchanged_by_position_side_field() {
let o = sample_order();
let s = serde_json::to_string(&o).unwrap();
assert_eq!(
s,
r#"{"owner":"0x0000000000000000000000000000000000000000","market":1,"side":"bid","kind":"limit","size":1000,"limit_px":5000000000000,"tif":"gtc","stp_mode":"cancel_oldest","reduce_only":false}"#
);
}
#[test]
fn hedge_order_serializes_position_side() {
for (ps, expected) in [(PositionSide::Long, "long"), (PositionSide::Short, "short")] {
let mut o = sample_order();
o.position_side = Some(ps);
let j = serde_json::to_value(&o).unwrap();
assert_eq!(j["position_side"], serde_json::json!(expected));
}
}
#[test]
fn position_side_serializes_snake_case() {
assert_eq!(
serde_json::to_string(&PositionSide::Long).unwrap(),
"\"long\""
);
assert_eq!(
serde_json::to_string(&PositionSide::Short).unwrap(),
"\"short\""
);
}
#[test]
fn order_serializes_builder_object() {
let mut o = sample_order();
let user = Address::from_hex("0x00000000000000000000000000000000000000ff").unwrap();
o.builder = Some(Builder { fee: 5, user });
let j = serde_json::to_value(&o).unwrap();
let b = j.get("builder").expect("builder key present");
assert_eq!(b["fee"], serde_json::json!(5));
assert!(b["fee"].is_number(), "fee must be a plain JSON number");
assert_eq!(
b["user"],
serde_json::json!("0x00000000000000000000000000000000000000ff")
);
}
#[test]
fn order_submit_shape_has_no_oid() {
let o = sample_order();
let j = serde_json::to_value(&o).unwrap();
assert!(
j.get("oid").is_none(),
"submit Order must not serialize oid"
);
}
#[test]
fn cancel_order_omits_none_fields() {
let c = CancelOrder {
owner: Address::ZERO,
market: MarketId(1),
oid: Some(OrderId(42)),
cloid: None,
};
let j = serde_json::to_value(&c).unwrap();
assert!(j.get("oid").is_some());
assert!(j.get("cloid").is_none());
}
#[test]
fn order_status_decodes_resting() {
let j = serde_json::json!({
"resting": { "oid": 12345, "cloid": "0x000102030405060708090a0b0c0d0e0f" }
});
let s: OrderStatus = serde_json::from_value(j).unwrap();
match &s {
OrderStatus::Resting(r) => {
assert_eq!(r.oid, OrderId(12345));
assert!(r.cloid.is_some());
}
other => panic!("expected Resting, got {other:?}"),
}
assert_eq!(s.oid(), Some(OrderId(12345)));
assert!(!s.is_error());
}
#[test]
fn order_status_decodes_resting_without_cloid() {
let j = serde_json::json!({ "resting": { "oid": 7 } });
let s: OrderStatus = serde_json::from_value(j).unwrap();
match s {
OrderStatus::Resting(r) => {
assert_eq!(r.oid, OrderId(7));
assert!(r.cloid.is_none());
}
other => panic!("expected Resting, got {other:?}"),
}
}
#[test]
fn order_status_decodes_filled_with_string_numerics() {
let j = serde_json::json!({
"filled": { "total_sz": "100000000", "avg_px": "10050000000", "oid": 12345 }
});
let s: OrderStatus = serde_json::from_value(j).unwrap();
match s {
OrderStatus::Filled(f) => {
assert_eq!(f.total_sz, "100000000");
assert_eq!(f.avg_px, "10050000000");
assert_eq!(f.oid, OrderId(12345));
}
other => panic!("expected Filled, got {other:?}"),
}
}
#[test]
fn order_status_decodes_error() {
let j = serde_json::json!({ "error": "px not tick-aligned" });
let s: OrderStatus = serde_json::from_value(j).unwrap();
assert!(s.is_error());
assert_eq!(s.oid(), None);
match s {
OrderStatus::Error(msg) => assert_eq!(msg, "px not tick-aligned"),
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn order_response_decodes_statuses_object() {
let j = serde_json::json!({ "statuses": [
{ "resting": { "oid": 12345, "cloid": "0x000102030405060708090a0b0c0d0e0f" } },
{ "filled": { "total_sz": "100000000", "avg_px": "10050000000", "oid": 12346 } },
{ "error": "size below market minimum" }
]});
let resp: OrderResponse = serde_json::from_value(j).unwrap();
assert_eq!(resp.statuses.len(), 3);
assert_eq!(
resp.first().and_then(OrderStatus::oid),
Some(OrderId(12345))
);
assert!(resp.statuses[2].is_error());
}
#[test]
fn order_response_round_trips() {
let resp = OrderResponse {
statuses: vec![
OrderStatus::Resting(RestingStatus {
oid: OrderId(1),
cloid: None,
}),
OrderStatus::Filled(FilledStatus {
total_sz: "5".into(),
avg_px: "100".into(),
oid: OrderId(2),
}),
OrderStatus::Error("nope".into()),
],
};
let j = serde_json::to_value(&resp).unwrap();
assert!(
j.is_object() && j.get("statuses").is_some(),
"OrderResponse wraps the per-order array under `statuses`"
);
let dec: OrderResponse = serde_json::from_value(j).unwrap();
assert_eq!(resp, dec);
}
#[test]
fn modify_omits_none_fields() {
let m = Modify {
market: MarketId(3),
oid: OrderId(42),
new_px: Some(1234),
new_size: None,
};
let j = serde_json::to_value(m).unwrap();
assert_eq!(j["new_px"], serde_json::json!(1234));
assert!(j.get("new_size").is_none());
assert!(j["oid"].is_number(), "oid is a plain integer");
}
#[test]
fn order_grouping_serializes_camel_case() {
assert_eq!(serde_json::to_string(&OrderGrouping::Na).unwrap(), "\"na\"");
assert_eq!(
serde_json::to_string(&OrderGrouping::NormalTpsl).unwrap(),
"\"normalTpsl\""
);
assert_eq!(
serde_json::to_string(&OrderGrouping::PositionTpsl).unwrap(),
"\"positionTpsl\""
);
assert_eq!(OrderGrouping::default(), OrderGrouping::Na);
}
#[test]
fn cancel_by_cloid_serializes_hex_cloid() {
let c = CancelByCloid {
asset: MarketId(7),
cloid: Cloid([0xAB; 16]),
};
let j = serde_json::to_value(c).unwrap();
assert_eq!(j["asset"], serde_json::json!(7));
assert_eq!(
j["cloid"],
serde_json::json!("0xabababababababababababababababab")
);
}
#[test]
fn cancel_all_orders_omits_none_asset() {
let all = CancelAllOrders { asset: None };
assert!(serde_json::to_value(all).unwrap().get("asset").is_none());
let one = CancelAllOrders {
asset: Some(MarketId(3)),
};
assert_eq!(
serde_json::to_value(one).unwrap()["asset"],
serde_json::json!(3)
);
}
}