use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
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 {
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(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderSide {
#[serde(rename = "buy")]
Buy,
#[serde(rename = "sell")]
Sell,
}
impl OrderSide {
pub fn as_str(&self) -> &'static str {
match self {
OrderSide::Buy => "buy",
OrderSide::Sell => "sell",
}
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderType {
#[serde(rename = "limit")]
Limit,
#[serde(rename = "market")]
Market,
#[serde(rename = "stop_limit")]
StopLimit,
#[serde(rename = "stop_market")]
StopMarket,
#[serde(rename = "take_limit")]
TakeLimit,
#[serde(rename = "take_market")]
TakeMarket,
#[serde(rename = "market_limit")]
MarketLimit,
#[serde(rename = "trailing_stop")]
TrailingStop,
}
impl OrderType {
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(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct NewOrderRequest {
pub symbol: String,
pub side: OrderSide,
pub order_type: OrderType,
pub quantity: f64,
pub price: Option<f64>,
pub time_in_force: TimeInForce,
pub client_order_id: Option<String>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderStatus {
New,
PartiallyFilled,
Filled,
DoneForDay,
Canceled,
Replaced,
PendingCancel,
Stopped,
Rejected,
Suspended,
PendingNew,
Calculated,
Expired,
AcceptedForBidding,
PendingReplace,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct OrderInfo {
pub amount: f64,
pub api: bool,
pub average_price: f64,
pub creation_timestamp: u64,
pub direction: String,
pub filled_amount: f64,
pub instrument_name: String,
pub is_liquidation: bool,
pub label: String,
pub last_update_timestamp: u64,
pub max_show: Option<f64>,
pub order_id: String,
pub order_state: String,
pub order_type: String,
pub original_order_type: Option<String>,
pub post_only: bool,
pub price: f64,
pub profit_loss: Option<f64>,
pub reduce_only: bool,
pub replaced: bool,
pub risk_reducing: bool,
pub time_in_force: String,
pub triggered: Option<bool>,
pub trigger: Option<String>,
pub usd: Option<f64>,
pub web: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_in_force_as_str() {
assert_eq!(TimeInForce::GoodTilCancelled.as_str(), "good_til_cancelled");
assert_eq!(TimeInForce::GoodTilDay.as_str(), "good_til_day");
assert_eq!(TimeInForce::FillOrKill.as_str(), "fill_or_kill");
assert_eq!(
TimeInForce::ImmediateOrCancel.as_str(),
"immediate_or_cancel"
);
}
#[test]
fn test_time_in_force_serialization() {
let tif = TimeInForce::GoodTilCancelled;
let json = serde_json::to_string(&tif).unwrap();
assert_eq!(json, "\"good_til_cancelled\"");
let deserialized: TimeInForce = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, TimeInForce::GoodTilCancelled);
}
#[test]
fn test_order_side_serialization() {
let buy_side = OrderSide::Buy;
let sell_side = OrderSide::Sell;
let buy_json = serde_json::to_string(&buy_side).unwrap();
let sell_json = serde_json::to_string(&sell_side).unwrap();
assert_eq!(buy_json, "\"buy\"");
assert_eq!(sell_json, "\"sell\"");
let buy_deserialized: OrderSide = serde_json::from_str(&buy_json).unwrap();
let sell_deserialized: OrderSide = serde_json::from_str(&sell_json).unwrap();
assert_eq!(buy_deserialized, OrderSide::Buy);
assert_eq!(sell_deserialized, OrderSide::Sell);
}
#[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::StopMarket.as_str(), "stop_market");
assert_eq!(OrderType::TakeLimit.as_str(), "take_limit");
assert_eq!(OrderType::TakeMarket.as_str(), "take_market");
assert_eq!(OrderType::MarketLimit.as_str(), "market_limit");
assert_eq!(OrderType::TrailingStop.as_str(), "trailing_stop");
}
#[test]
fn test_order_type_serialization() {
let order_type = OrderType::Limit;
let json = serde_json::to_string(&order_type).unwrap();
assert_eq!(json, "\"limit\"");
let deserialized: OrderType = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, OrderType::Limit);
}
#[test]
fn test_new_order_request_creation() {
let order = NewOrderRequest {
symbol: "BTC-PERPETUAL".to_string(),
side: OrderSide::Buy,
order_type: OrderType::Limit,
quantity: 1.0,
price: Some(50000.0),
time_in_force: TimeInForce::GoodTilCancelled,
client_order_id: Some("CLIENT_ORDER_123".to_string()),
};
assert_eq!(order.symbol, "BTC-PERPETUAL");
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.order_type, OrderType::Limit);
assert_eq!(order.quantity, 1.0);
assert_eq!(order.price, Some(50000.0));
assert_eq!(order.time_in_force, TimeInForce::GoodTilCancelled);
assert_eq!(order.client_order_id, Some("CLIENT_ORDER_123".to_string()));
}
#[test]
fn test_new_order_request_market_order() {
let market_order = NewOrderRequest {
symbol: "ETH-PERPETUAL".to_string(),
side: OrderSide::Sell,
order_type: OrderType::Market,
quantity: 2.0,
price: None, time_in_force: TimeInForce::ImmediateOrCancel,
client_order_id: None,
};
assert_eq!(market_order.order_type, OrderType::Market);
assert_eq!(market_order.price, None);
assert_eq!(market_order.client_order_id, None);
}
#[test]
fn test_order_status_variants() {
let statuses = vec![
OrderStatus::New,
OrderStatus::PartiallyFilled,
OrderStatus::Filled,
OrderStatus::DoneForDay,
OrderStatus::Canceled,
OrderStatus::Replaced,
OrderStatus::PendingCancel,
OrderStatus::Stopped,
OrderStatus::Rejected,
OrderStatus::Suspended,
OrderStatus::PendingNew,
OrderStatus::Calculated,
OrderStatus::Expired,
OrderStatus::AcceptedForBidding,
OrderStatus::PendingReplace,
];
for status in statuses {
let cloned = status;
assert_eq!(status, cloned);
}
}
#[test]
fn test_order_info_creation() {
let order_info = OrderInfo {
amount: 1.0,
api: true,
average_price: 50000.0,
creation_timestamp: 1640995200000,
direction: "buy".to_string(),
filled_amount: 0.5,
instrument_name: "BTC-PERPETUAL".to_string(),
is_liquidation: false,
label: "test_order".to_string(),
last_update_timestamp: 1640995300000,
max_show: Some(0.8),
order_id: "ORDER_123".to_string(),
order_state: "open".to_string(),
order_type: "limit".to_string(),
original_order_type: None,
post_only: false,
price: 50000.0,
profit_loss: Some(100.0),
reduce_only: false,
replaced: false,
risk_reducing: false,
time_in_force: "good_til_cancelled".to_string(),
triggered: Some(false),
trigger: None,
usd: Some(50000.0),
web: false,
};
assert_eq!(order_info.amount, 1.0);
assert_eq!(order_info.instrument_name, "BTC-PERPETUAL");
assert_eq!(order_info.filled_amount, 0.5);
assert_eq!(order_info.max_show, Some(0.8));
assert_eq!(order_info.profit_loss, Some(100.0));
assert!(order_info.api);
assert!(!order_info.is_liquidation);
}
#[test]
fn test_order_info_optional_fields() {
let minimal_order_info = OrderInfo {
amount: 1.0,
api: true,
average_price: 0.0,
creation_timestamp: 1640995200000,
direction: "buy".to_string(),
filled_amount: 0.0,
instrument_name: "BTC-PERPETUAL".to_string(),
is_liquidation: false,
label: "".to_string(),
last_update_timestamp: 1640995200000,
max_show: None,
order_id: "ORDER_123".to_string(),
order_state: "new".to_string(),
order_type: "limit".to_string(),
original_order_type: None,
post_only: false,
price: 50000.0,
profit_loss: None,
reduce_only: false,
replaced: false,
risk_reducing: false,
time_in_force: "good_til_cancelled".to_string(),
triggered: None,
trigger: None,
usd: None,
web: false,
};
assert_eq!(minimal_order_info.max_show, None);
assert_eq!(minimal_order_info.profit_loss, None);
assert_eq!(minimal_order_info.triggered, None);
assert_eq!(minimal_order_info.trigger, None);
assert_eq!(minimal_order_info.usd, None);
}
#[test]
fn test_serialization_roundtrip() {
let order = NewOrderRequest {
symbol: "BTC-PERPETUAL".to_string(),
side: OrderSide::Buy,
order_type: OrderType::Limit,
quantity: 1.0,
price: Some(50000.0),
time_in_force: TimeInForce::GoodTilCancelled,
client_order_id: Some("CLIENT_ORDER_123".to_string()),
};
let json = serde_json::to_string(&order).unwrap();
let deserialized: NewOrderRequest = serde_json::from_str(&json).unwrap();
assert_eq!(order.symbol, deserialized.symbol);
assert_eq!(order.side, deserialized.side);
assert_eq!(order.order_type, deserialized.order_type);
assert_eq!(order.quantity, deserialized.quantity);
assert_eq!(order.price, deserialized.price);
assert_eq!(order.time_in_force, deserialized.time_in_force);
assert_eq!(order.client_order_id, deserialized.client_order_id);
}
#[test]
fn test_debug_and_display_implementations() {
let order = NewOrderRequest {
symbol: "BTC-PERPETUAL".to_string(),
side: OrderSide::Buy,
order_type: OrderType::Limit,
quantity: 1.0,
price: Some(50000.0),
time_in_force: TimeInForce::GoodTilCancelled,
client_order_id: Some("CLIENT_ORDER_123".to_string()),
};
let debug_str = format!("{:?}", order);
let display_str = format!("{}", order);
assert!(debug_str.contains("BTC-PERPETUAL"));
assert!(display_str.contains("BTC-PERPETUAL"));
}
#[test]
fn test_enum_equality_and_cloning() {
let tif1 = TimeInForce::GoodTilCancelled;
let tif2 = tif1;
assert_eq!(tif1, tif2);
let side1 = OrderSide::Buy;
let side2 = side1;
assert_eq!(side1, side2);
let type1 = OrderType::Limit;
let type2 = type1;
assert_eq!(type1, type2);
let status1 = OrderStatus::New;
let status2 = status1;
assert_eq!(status1, status2);
}
#[test]
fn test_order_type_variants_coverage() {
let types = vec![
OrderType::Limit,
OrderType::Market,
OrderType::StopLimit,
OrderType::StopMarket,
OrderType::TakeLimit,
OrderType::TakeMarket,
OrderType::MarketLimit,
OrderType::TrailingStop,
];
for order_type in types {
let str_repr = order_type.as_str();
assert!(!str_repr.is_empty());
let json = serde_json::to_string(&order_type).unwrap();
let deserialized: OrderType = serde_json::from_str(&json).unwrap();
assert_eq!(order_type, deserialized);
}
}
#[test]
fn test_time_in_force_variants_coverage() {
let tifs = vec![
TimeInForce::GoodTilCancelled,
TimeInForce::GoodTilDay,
TimeInForce::FillOrKill,
TimeInForce::ImmediateOrCancel,
];
for tif in tifs {
let str_repr = tif.as_str();
assert!(!str_repr.is_empty());
let json = serde_json::to_string(&tif).unwrap();
let deserialized: TimeInForce = serde_json::from_str(&json).unwrap();
assert_eq!(tif, deserialized);
}
}
}