use std::{fmt, str::FromStr};
use rust_decimal::{Decimal, prelude::FromPrimitive};
use serde::{Deserialize, Deserializer, Serialize, de::Visitor};
use strum::{Display, EnumString};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TriggerBy {
LastPrice,
MarkPrice,
IndexPrice,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TriggerQuantity {
Percent(Decimal),
Amount(Decimal),
}
impl<'de> Deserialize<'de> for TriggerQuantity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct QtyVisitor;
impl Visitor<'_> for QtyVisitor {
type Value = TriggerQuantity;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(r#"a string like "12.5%" or "0.01", or a number"#)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
parse_str(v).map_err(serde::de::Error::custom)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Decimal::from_f64(v)
.ok_or_else(|| serde::de::Error::custom("not a finite number"))
.map(TriggerQuantity::Amount)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TriggerQuantity::Amount(Decimal::from(v)))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(TriggerQuantity::Amount(Decimal::from(v)))
}
}
deserializer.deserialize_any(QtyVisitor)
}
}
impl Serialize for TriggerQuantity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(
match self {
Self::Percent(percent) => format!("{percent}%"),
Self::Amount(amount) => format!("{amount}"),
}
.as_str(),
)
}
}
fn parse_str(s: &str) -> Result<TriggerQuantity, &'static str> {
if let Some(num) = s.strip_suffix('%') {
let d = Decimal::from_str(num.trim()).map_err(|_| "invalid percent value")?;
Ok(TriggerQuantity::Percent(d))
} else {
let d = Decimal::from_str(s.trim()).map_err(|_| "invalid decimal value")?;
Ok(TriggerQuantity::Amount(d))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketOrder {
pub id: String,
pub client_id: Option<u32>,
pub symbol: String,
pub side: Side,
pub quantity: Option<Decimal>,
pub executed_quantity: Decimal,
pub quote_quantity: Option<Decimal>,
pub executed_quote_quantity: Decimal,
pub stop_loss_trigger_price: Option<Decimal>,
pub stop_loss_limit_price: Option<Decimal>,
pub stop_loss_trigger_by: Option<TriggerBy>,
pub take_profit_trigger_price: Option<Decimal>,
pub take_profit_limit_price: Option<Decimal>,
pub take_profit_trigger_by: Option<TriggerBy>,
pub trigger_by: Option<TriggerBy>,
pub trigger_price: Option<Decimal>,
pub trigger_quantity: Option<TriggerQuantity>,
pub triggered_at: Option<i64>,
pub time_in_force: TimeInForce,
pub related_order_id: Option<String>,
pub self_trade_prevention: SelfTradePrevention,
pub reduce_only: Option<bool>,
pub status: OrderStatus,
pub created_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LimitOrder {
pub id: String,
pub client_id: Option<u32>,
pub symbol: String,
pub side: Side,
pub quantity: Decimal,
pub executed_quantity: Decimal,
pub executed_quote_quantity: Decimal,
pub stop_loss_trigger_price: Option<Decimal>,
pub stop_loss_limit_price: Option<Decimal>,
pub stop_loss_trigger_by: Option<TriggerBy>,
pub take_profit_trigger_price: Option<Decimal>,
pub take_profit_limit_price: Option<Decimal>,
pub take_profit_trigger_by: Option<TriggerBy>,
pub price: Decimal,
pub trigger_by: Option<TriggerBy>,
pub trigger_price: Option<Decimal>,
pub trigger_quantity: Option<TriggerQuantity>,
pub triggered_at: Option<i64>,
pub time_in_force: TimeInForce,
pub related_order_id: Option<String>,
pub self_trade_prevention: SelfTradePrevention,
pub post_only: bool,
pub reduce_only: Option<bool>,
pub status: OrderStatus,
pub created_at: i64,
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum OrderType {
#[default]
#[serde(rename(deserialize = "LIMIT"))]
Limit,
#[serde(rename(deserialize = "MARKET"))]
Market,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "orderType")]
pub enum Order {
Market(MarketOrder),
Limit(LimitOrder),
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "UPPERCASE")]
#[serde(rename_all = "UPPERCASE")]
pub enum TimeInForce {
#[default]
GTC,
IOC,
FOK,
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum SelfTradePrevention {
#[default]
RejectTaker,
RejectMaker,
RejectBoth,
Allow,
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum OrderStatus {
Cancelled,
Expired,
Filled,
#[default]
New,
PartiallyFilled,
Triggered,
TriggerPending,
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum SystemOrderType {
#[default]
LiquidatePositionOnBook,
LiquidatePositionOnBackstop,
LiquidatePositionOnAdl,
CollateralConversion,
FutureExpiry,
OrderBookClosed,
}
#[derive(
Debug, Display, Clone, Copy, Serialize, Deserialize, Default, EnumString, PartialEq, Eq, Hash,
)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum Side {
#[default]
Bid,
Ask,
}
#[derive(Debug, Display, Clone, Copy, Serialize, Deserialize, EnumString, PartialEq, Eq, Hash)]
#[strum(serialize_all = "PascalCase")]
#[serde(rename_all = "PascalCase")]
pub enum SlippageToleranceType {
TickSize,
Percent,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteOrderPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_lend: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_lend_redeem: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_borrow: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_borrow_repay: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<u32>,
pub order_type: OrderType,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantity: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_quantity: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub self_trade_prevention: Option<SelfTradePrevention>,
pub side: Side,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss_limit_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss_trigger_by: Option<TriggerBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_loss_trigger_price: Option<Decimal>,
pub symbol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit_limit_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit_trigger_by: Option<TriggerBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub take_profit_trigger_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<TimeInForce>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_by: Option<TriggerBy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_quantity: Option<TriggerQuantity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slippage_tolerance: Option<Decimal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slippage_tolerance_type: Option<SlippageToleranceType>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CancelOrderPayload {
pub symbol: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CancelOpenOrdersPayload {
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderUpdateType {
OrderAccepted,
OrderCancelled,
OrderExpired,
OrderFill,
OrderModified,
TriggerPlaced,
TriggerFailed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderUpdate {
#[serde(rename = "e")]
pub event_type: OrderUpdateType,
#[serde(rename = "E")]
pub event_time: i64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "c")]
pub client_order_id: Option<u64>,
#[serde(rename = "S")]
pub side: Side,
#[serde(rename = "o")]
pub order_type: OrderType,
#[serde(rename = "f")]
pub time_in_force: TimeInForce,
#[serde(rename = "q")]
pub quantity: Decimal,
#[serde(rename = "Q")]
pub quantity_in_quote: Option<Decimal>,
#[serde(rename = "p")]
pub price: Option<Decimal>,
#[serde(rename = "P")]
pub trigger_price: Option<Decimal>,
#[serde(rename = "B")]
pub trigger_by: Option<TriggerBy>,
#[serde(rename = "a")]
pub take_profit_trigger_price: Option<Decimal>,
#[serde(rename = "b")]
pub stop_loss_trigger_price: Option<Decimal>,
#[serde(rename = "d")]
pub take_profit_trigger_by: Option<TriggerBy>,
#[serde(rename = "g")]
pub stop_loss_trigger_by: Option<TriggerBy>,
#[serde(rename = "Y")]
pub trigger_quantity: Option<TriggerQuantity>,
#[serde(rename = "X")]
pub order_status: OrderStatus,
#[serde(rename = "R")]
pub order_expiry_reason: Option<String>,
#[serde(rename = "i")]
pub order_id: String,
#[serde(rename = "t")]
pub trade_id: Option<u64>,
#[serde(rename = "l")]
pub fill_quantity: Option<Decimal>,
#[serde(rename = "z")]
pub executed_quantity: Decimal,
#[serde(rename = "Z")]
pub executed_quantity_in_quote: Decimal,
#[serde(rename = "L")]
pub fill_price: Option<Decimal>,
#[serde(rename = "m")]
pub was_maker: Option<bool>,
#[serde(rename = "n")]
pub fee: Option<Decimal>,
#[serde(rename = "N")]
pub fee_symbol: Option<String>,
#[serde(rename = "V")]
pub self_trade_prevention: SelfTradePrevention,
#[serde(rename = "T")]
pub timestamp: i64,
#[serde(rename = "O")]
pub origin_of_the_update: String,
#[serde(rename = "I")]
pub related_order_id: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderError {
pub code: String,
pub message: String,
pub operation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum BatchOrderResponse {
Order(Order),
Error(OrderError),
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
use serde_json::json;
#[test]
fn both_forms_round_trip() {
let q: TriggerQuantity = serde_json::from_value(json!("12.5%")).unwrap();
assert_eq!(q, TriggerQuantity::Percent(dec!(12.5)));
let q: TriggerQuantity = serde_json::from_value(json!("0.01")).unwrap();
assert_eq!(q, TriggerQuantity::Amount(dec!(0.01)));
}
#[test]
fn test_trigger_quantity_serialize() {
let trigger_quantity = TriggerQuantity::Percent(dec!(100));
let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
assert_eq!(trigger_quantity_str, "\"100%\"");
let trigger_quantity = TriggerQuantity::Percent(dec!(75.50));
let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
assert_eq!(trigger_quantity_str, "\"75.50%\"");
let trigger_quantity = TriggerQuantity::Amount(dec!(100));
let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
assert_eq!(trigger_quantity_str, "\"100\"");
let trigger_quantity = TriggerQuantity::Amount(dec!(75.50));
let trigger_quantity_str = serde_json::to_string(&trigger_quantity).unwrap();
assert_eq!(trigger_quantity_str, "\"75.50\"");
}
#[test]
fn test_trigger_by_serialize() {
let trigger_by_last = TriggerBy::LastPrice;
let trigger_by_last_str = serde_json::to_string(&trigger_by_last).unwrap();
assert_eq!(trigger_by_last_str, "\"LastPrice\"");
let trigger_by_mark = TriggerBy::MarkPrice;
let trigger_by_mark_str = serde_json::to_string(&trigger_by_mark).unwrap();
assert_eq!(trigger_by_mark_str, "\"MarkPrice\"");
let trigger_by_index = TriggerBy::IndexPrice;
let trigger_by_index_str = serde_json::to_string(&trigger_by_index).unwrap();
assert_eq!(trigger_by_index_str, "\"IndexPrice\"");
}
#[test]
fn test_order_update() {
let data = r#"
{"E":1748288167010366,"O":"USER","P":"178.05","Q":"0","S":"Ask","T":1748288167009460,"V":"RejectTaker","X":"TriggerPending","Y":"20.03","Z":"0","e":"triggerPlaced","f":"GTC","i":"114575813313101824","o":"LIMIT","p":"178.15","q":"0","r":false,"s":"SOL_USDC","t":null,"z":"0"}
"#;
let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
assert_eq!(order_update.price.unwrap(), dec!(178.15));
assert_eq!(order_update.trigger_price.unwrap(), dec!(178.05));
assert_eq!(
order_update.trigger_quantity.unwrap(),
TriggerQuantity::Amount(dec!(20.03))
);
assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(0));
let data = r#"
{"E":1748288615134547,"O":"USER","Q":"3568.3445","S":"Ask","T":1748288615133255,"V":"RejectTaker","X":"New","Z":"0","e":"orderAccepted","f":"GTC","i":"114575842681290753","o":"LIMIT","p":"178.15","q":"20.03","r":false,"s":"SOL_USDC","t":null,"z":"0"}
"#;
let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
assert_eq!(order_update.price.unwrap(), dec!(178.15));
assert_eq!(order_update.trigger_price, None);
assert_eq!(order_update.quantity_in_quote.unwrap(), dec!(3568.3445));
assert_eq!(order_update.quantity, dec!(20.03));
let data = r#"
{"B":"LastPrice","E":1748289564405220,"O":"USER","P":"178.55","S":"Ask","T":1748289564404373,"V":"RejectTaker","X":"Cancelled","Y":"80%","Z":"0","e":"orderCancelled","f":"GTC","i":"114575904705282048","o":"MARKET","q":"0","r":false,"s":"SOL_USDC","t":null,"z":"0"}
"#;
let order_update: OrderUpdate = serde_json::from_str(data).unwrap();
assert_eq!(order_update.trigger_price.unwrap(), dec!(178.55));
assert_eq!(
order_update.trigger_quantity.unwrap(),
TriggerQuantity::Percent(dec!(80))
);
}
}