use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
#[serde(rename_all = "snake_case")]
pub enum OrderType {
Limit,
Market,
StopLimit,
StopMarket,
TakeLimit,
TakeMarket,
MarketLimit,
TrailingStop,
}
impl OrderType {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
OrderType::Limit => "limit",
OrderType::Market => "market",
OrderType::StopLimit => "stop_limit",
OrderType::StopMarket => "stop_market",
OrderType::TakeLimit => "take_limit",
OrderType::TakeMarket => "take_market",
OrderType::MarketLimit => "market_limit",
OrderType::TrailingStop => "trailing_stop",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
#[serde(rename_all = "snake_case")]
pub enum TimeInForce {
#[serde(rename = "good_til_cancelled")]
GoodTilCancelled,
#[serde(rename = "good_til_day")]
GoodTilDay,
#[serde(rename = "fill_or_kill")]
FillOrKill,
#[serde(rename = "immediate_or_cancel")]
ImmediateOrCancel,
}
impl TimeInForce {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
TimeInForce::GoodTilCancelled => "good_til_cancelled",
TimeInForce::GoodTilDay => "good_til_day",
TimeInForce::FillOrKill => "fill_or_kill",
TimeInForce::ImmediateOrCancel => "immediate_or_cancel",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, DebugPretty, DisplaySimple)]
#[serde(rename_all = "snake_case")]
pub enum Trigger {
IndexPrice,
MarkPrice,
LastPrice,
}
#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
pub struct OrderRequest {
pub instrument_name: String,
pub amount: f64,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub order_type: Option<OrderType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<TimeInForce>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_show: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger: Option<Trigger>,
#[serde(skip_serializing_if = "Option::is_none")]
pub advanced: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mmp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_until: Option<u64>,
}
impl OrderRequest {
#[must_use]
pub fn limit(instrument_name: String, amount: f64, price: f64) -> Self {
Self {
instrument_name,
amount,
order_type: Some(OrderType::Limit),
label: None,
price: Some(price),
time_in_force: None,
max_show: None,
post_only: None,
reduce_only: None,
trigger_price: None,
trigger: None,
advanced: None,
mmp: None,
valid_until: None,
}
}
#[must_use]
pub fn market(instrument_name: String, amount: f64) -> Self {
Self {
instrument_name,
amount,
order_type: Some(OrderType::Market),
label: None,
price: None,
time_in_force: None,
max_show: None,
post_only: None,
reduce_only: None,
trigger_price: None,
trigger: None,
advanced: None,
mmp: None,
valid_until: None,
}
}
#[must_use]
pub fn with_label(mut self, label: String) -> Self {
self.label = Some(label);
self
}
#[must_use]
pub fn with_time_in_force(mut self, tif: TimeInForce) -> Self {
self.time_in_force = Some(tif);
self
}
#[must_use]
pub fn with_post_only(mut self, post_only: bool) -> Self {
self.post_only = Some(post_only);
self
}
#[must_use]
pub fn with_reduce_only(mut self, reduce_only: bool) -> Self {
self.reduce_only = Some(reduce_only);
self
}
#[must_use]
pub fn with_max_show(mut self, max_show: f64) -> Self {
self.max_show = Some(max_show);
self
}
#[must_use]
pub fn with_trigger(mut self, trigger_price: f64, trigger: Trigger) -> Self {
self.trigger_price = Some(trigger_price);
self.trigger = Some(trigger);
self
}
#[must_use]
pub fn with_mmp(mut self, mmp: bool) -> Self {
self.mmp = Some(mmp);
self
}
}
#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
pub struct EditOrderRequest {
pub order_id: String,
pub amount: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduce_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub advanced: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mmp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_until: Option<u64>,
}
impl EditOrderRequest {
#[must_use]
pub fn new(order_id: String, amount: f64) -> Self {
Self {
order_id,
amount,
price: None,
post_only: None,
reduce_only: None,
advanced: None,
trigger_price: None,
mmp: None,
valid_until: None,
}
}
#[must_use]
pub fn with_price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
#[must_use]
pub fn with_post_only(mut self, post_only: bool) -> Self {
self.post_only = Some(post_only);
self
}
#[must_use]
pub fn with_reduce_only(mut self, reduce_only: bool) -> Self {
self.reduce_only = Some(reduce_only);
self
}
}
#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
pub struct TradeExecution {
pub trade_id: String,
pub instrument_name: String,
pub direction: String,
pub amount: f64,
pub price: f64,
pub fee: f64,
pub fee_currency: String,
pub order_id: String,
pub order_type: String,
pub timestamp: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub liquidity: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mark_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profit_loss: Option<f64>,
}
#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
pub struct OrderInfo {
pub order_id: String,
pub instrument_name: String,
pub direction: String,
pub amount: f64,
#[serde(default)]
pub filled_amount: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub average_price: Option<f64>,
pub order_type: String,
pub order_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_in_force: Option<String>,
#[serde(default)]
pub label: String,
pub creation_timestamp: u64,
pub last_update_timestamp: u64,
#[serde(default)]
pub api: bool,
#[serde(default)]
pub web: bool,
#[serde(default)]
pub post_only: bool,
#[serde(default)]
pub reduce_only: bool,
#[serde(default)]
pub is_liquidation: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_show: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profit_loss: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usd: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implv: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_price: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub triggered: Option<bool>,
#[serde(default)]
pub replaced: bool,
#[serde(default)]
pub mmp: bool,
#[serde(default)]
pub mmp_cancelled: bool,
}
#[derive(Clone, Serialize, Deserialize, DebugPretty, DisplaySimple)]
pub struct OrderResponse {
pub order: OrderInfo,
#[serde(default)]
pub trades: Vec<TradeExecution>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_order_type_serialization() {
let order_type = OrderType::Limit;
let json = serde_json::to_string(&order_type).expect("serialize");
assert_eq!(json, "\"limit\"");
let order_type = OrderType::StopLimit;
let json = serde_json::to_string(&order_type).expect("serialize");
assert_eq!(json, "\"stop_limit\"");
}
#[test]
fn test_time_in_force_serialization() {
let tif = TimeInForce::GoodTilCancelled;
let json = serde_json::to_string(&tif).expect("serialize");
assert_eq!(json, "\"good_til_cancelled\"");
let tif = TimeInForce::ImmediateOrCancel;
let json = serde_json::to_string(&tif).expect("serialize");
assert_eq!(json, "\"immediate_or_cancel\"");
}
#[test]
fn test_order_request_limit() {
let request = OrderRequest::limit("BTC-PERPETUAL".to_string(), 100.0, 50000.0)
.with_label("test_order".to_string())
.with_post_only(true);
assert_eq!(request.instrument_name, "BTC-PERPETUAL");
assert_eq!(request.amount, 100.0);
assert_eq!(request.price, Some(50000.0));
assert_eq!(request.order_type, Some(OrderType::Limit));
assert_eq!(request.label, Some("test_order".to_string()));
assert_eq!(request.post_only, Some(true));
}
#[test]
fn test_order_request_market() {
let request =
OrderRequest::market("ETH-PERPETUAL".to_string(), 10.0).with_reduce_only(true);
assert_eq!(request.instrument_name, "ETH-PERPETUAL");
assert_eq!(request.amount, 10.0);
assert_eq!(request.price, None);
assert_eq!(request.order_type, Some(OrderType::Market));
assert_eq!(request.reduce_only, Some(true));
}
#[test]
fn test_edit_order_request() {
let request = EditOrderRequest::new("order123".to_string(), 200.0).with_price(51000.0);
assert_eq!(request.order_id, "order123");
assert_eq!(request.amount, 200.0);
assert_eq!(request.price, Some(51000.0));
}
#[test]
fn test_order_type_as_str() {
assert_eq!(OrderType::Limit.as_str(), "limit");
assert_eq!(OrderType::Market.as_str(), "market");
assert_eq!(OrderType::StopLimit.as_str(), "stop_limit");
assert_eq!(OrderType::TrailingStop.as_str(), "trailing_stop");
}
#[test]
fn test_time_in_force_as_str() {
assert_eq!(TimeInForce::GoodTilCancelled.as_str(), "good_til_cancelled");
assert_eq!(TimeInForce::FillOrKill.as_str(), "fill_or_kill");
assert_eq!(
TimeInForce::ImmediateOrCancel.as_str(),
"immediate_or_cancel"
);
}
}