use serde::{Deserialize, Serialize};
use crate::types::Cloid;
use crate::types::order::{Side, StpMode, TimeInForce};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotOrder {
pub pair: u32,
pub side: Side,
pub size: u64,
pub limit_px: u64,
pub tif: TimeInForce,
pub stp_mode: StpMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloid: Option<Cloid>,
}
impl SpotOrder {
#[must_use]
pub const fn ioc_limit(pair: u32, side: Side, size: u64, limit_px: u64) -> Self {
Self {
pair,
side,
size,
limit_px,
tif: TimeInForce::Ioc,
stp_mode: StpMode::CancelOldest,
cloid: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotCancel {
pub pair: u32,
pub oid: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotMarginDeposit {
pub pair: u32,
pub amount: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotMarginWithdraw {
pub pair: u32,
pub amount: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotMarginOpen {
pub pair: u32,
pub size: u64,
pub limit_px: u64,
pub borrow: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SpotMarginClose {
pub pair: u32,
pub limit_px: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EarnDeposit {
pub asset: u32,
pub amount: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct EarnWithdraw {
pub asset: u32,
pub shares: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spot_order_ioc_limit_defaults() {
let o = SpotOrder::ioc_limit(3, Side::Bid, 1000, 5_000_000_000);
assert_eq!(o.tif, TimeInForce::Ioc);
assert_eq!(o.stp_mode, StpMode::CancelOldest);
assert!(o.cloid.is_none());
}
#[test]
fn spot_order_serializes_snake_case_integers() {
let o = SpotOrder::ioc_limit(3, Side::Ask, 1000, 5_000_000_000);
let j = serde_json::to_value(&o).unwrap();
assert!(j["pair"].is_number());
assert!(j["size"].is_number());
assert!(j["limit_px"].is_number(), "limit_px must be a plain number");
assert_eq!(j["side"], serde_json::json!("ask"));
assert_eq!(j["tif"], serde_json::json!("ioc"));
assert_eq!(j["stp_mode"], serde_json::json!("cancel_oldest"));
assert!(j.get("limitPx").is_none(), "no camelCase leak");
}
#[test]
fn spot_order_omits_none_cloid() {
let o = SpotOrder::ioc_limit(1, Side::Bid, 1, 1);
let j = serde_json::to_value(&o).unwrap();
assert!(j.get("cloid").is_none());
}
#[test]
fn spot_order_serializes_cloid_when_set() {
let mut o = SpotOrder::ioc_limit(1, Side::Bid, 1, 1);
o.cloid = Some(Cloid([0xCDu8; 16]));
let j = serde_json::to_value(&o).unwrap();
assert_eq!(
j["cloid"],
serde_json::json!("0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd")
);
}
#[test]
fn spot_order_round_trips() {
let mut o = SpotOrder::ioc_limit(7, Side::Ask, 42, 9_999);
o.stp_mode = StpMode::CancelNewest;
o.cloid = Some(Cloid([0x01u8; 16]));
let j = serde_json::to_string(&o).unwrap();
let dec: SpotOrder = serde_json::from_str(&j).unwrap();
assert_eq!(o, dec);
}
#[test]
fn spot_cancel_serializes_snake_case() {
let c = SpotCancel {
pair: 3,
oid: 12345,
};
let j = serde_json::to_value(c).unwrap();
assert_eq!(j["pair"], serde_json::json!(3));
assert_eq!(j["oid"], serde_json::json!(12345));
let dec: SpotCancel = serde_json::from_value(j).unwrap();
assert_eq!(c, dec);
}
#[test]
fn spot_margin_deposit_decimal_rides_as_json_string() {
let d = SpotMarginDeposit {
pair: 200,
amount: "100".into(),
};
let j = serde_json::to_value(&d).unwrap();
assert_eq!(j["pair"], serde_json::json!(200));
assert!(
j["amount"].is_string(),
"decimal amount must be a JSON string"
);
assert_eq!(j["amount"], serde_json::json!("100"));
let dec: SpotMarginDeposit = serde_json::from_value(j).unwrap();
assert_eq!(d, dec);
}
#[test]
fn spot_margin_open_mixes_integer_planes_and_decimal_string() {
let o = SpotMarginOpen {
pair: 200,
size: 200,
limit_px: 200_000_000,
borrow: "400".into(),
};
let j = serde_json::to_value(&o).unwrap();
assert!(j["size"].is_number(), "size is a raw-lot integer");
assert!(j["limit_px"].is_number(), "limit_px is a 1e8-plane integer");
assert_eq!(j["limit_px"], serde_json::json!(200_000_000));
assert!(j["borrow"].is_string(), "borrow is a decimal JSON string");
assert!(j.get("limitPx").is_none(), "no camelCase leak");
let dec: SpotMarginOpen = serde_json::from_value(j).unwrap();
assert_eq!(o, dec);
}
#[test]
fn spot_margin_close_serializes_snake_case() {
let c = SpotMarginClose {
pair: 200,
limit_px: 190_000_000,
};
let j = serde_json::to_value(c).unwrap();
assert_eq!(j["pair"], serde_json::json!(200));
assert_eq!(j["limit_px"], serde_json::json!(190_000_000));
let dec: SpotMarginClose = serde_json::from_value(j).unwrap();
assert_eq!(c, dec);
}
#[test]
fn earn_actions_serialize_asset_and_decimal_string() {
let d = EarnDeposit {
asset: 100,
amount: "5000".into(),
};
let jd = serde_json::to_value(&d).unwrap();
assert_eq!(jd["asset"], serde_json::json!(100));
assert_eq!(jd["amount"], serde_json::json!("5000"));
assert!(jd["amount"].is_string());
assert_eq!(d, serde_json::from_value::<EarnDeposit>(jd).unwrap());
let w = EarnWithdraw {
asset: 100,
shares: "1234.5".into(),
};
let jw = serde_json::to_value(&w).unwrap();
assert_eq!(jw["asset"], serde_json::json!(100));
assert_eq!(jw["shares"], serde_json::json!("1234.5"));
assert!(
jw["shares"].is_string(),
"fractional shares must survive as a string"
);
assert_eq!(w, serde_json::from_value::<EarnWithdraw>(jw).unwrap());
}
}