use serde::{Deserialize, Serialize};
#[derive(Debug, 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 {
#[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(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderSide {
Buy,
Sell,
}
#[derive(Debug, 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 {
#[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(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TriggerType {
#[serde(rename = "index_price")]
IndexPrice,
#[serde(rename = "mark_price")]
MarkPrice,
#[serde(rename = "last_price")]
LastPrice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AdvancedOrderType {
#[serde(rename = "usd")]
Usd,
#[serde(rename = "implv")]
Implv,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct NewOrderRequest {
pub instrument_name: String,
pub amount: f64,
#[serde(rename = "type")]
pub order_type: OrderType,
pub side: OrderSide,
pub price: Option<f64>,
pub time_in_force: TimeInForce,
pub post_only: Option<bool>,
pub reduce_only: Option<bool>,
pub label: Option<String>,
pub stop_price: Option<f64>,
pub trigger: Option<TriggerType>,
pub advanced: Option<AdvancedOrderType>,
pub max_show: Option<f64>,
pub reject_post_only: Option<bool>,
pub valid_until: Option<i64>,
pub client_order_id: Option<String>,
}
impl_json_display!(NewOrderRequest);
impl_json_debug_pretty!(NewOrderRequest);
impl NewOrderRequest {
#[must_use]
pub fn market_buy(instrument_name: String, amount: f64) -> Self {
Self {
instrument_name,
amount,
order_type: OrderType::Market,
side: OrderSide::Buy,
price: None,
time_in_force: TimeInForce::ImmediateOrCancel,
post_only: None,
reduce_only: None,
label: None,
stop_price: None,
trigger: None,
advanced: None,
max_show: None,
reject_post_only: None,
valid_until: None,
client_order_id: None,
}
}
#[must_use]
pub fn market_sell(instrument_name: String, amount: f64) -> Self {
Self {
instrument_name,
amount,
order_type: OrderType::Market,
side: OrderSide::Sell,
price: None,
time_in_force: TimeInForce::ImmediateOrCancel,
post_only: None,
reduce_only: None,
label: None,
stop_price: None,
trigger: None,
advanced: None,
max_show: None,
reject_post_only: None,
valid_until: None,
client_order_id: None,
}
}
#[must_use]
pub fn limit_buy(instrument_name: String, amount: f64, price: f64) -> Self {
Self {
instrument_name,
amount,
order_type: OrderType::Limit,
side: OrderSide::Buy,
price: Some(price),
time_in_force: TimeInForce::GoodTilCancelled,
post_only: None,
reduce_only: None,
label: None,
stop_price: None,
trigger: None,
advanced: None,
max_show: None,
reject_post_only: None,
valid_until: None,
client_order_id: None,
}
}
#[must_use]
pub fn limit_sell(instrument_name: String, amount: f64, price: f64) -> Self {
Self {
instrument_name,
amount,
order_type: OrderType::Limit,
side: OrderSide::Sell,
price: Some(price),
time_in_force: TimeInForce::GoodTilCancelled,
post_only: None,
reduce_only: None,
label: None,
stop_price: None,
trigger: None,
advanced: None,
max_show: None,
reject_post_only: None,
valid_until: None,
client_order_id: None,
}
}
#[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_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 = tif;
self
}
#[must_use]
pub fn with_client_order_id(mut self, client_order_id: String) -> Self {
self.client_order_id = Some(client_order_id);
self
}
}
#[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_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");
}
#[test]
fn test_new_order_request_market_buy() {
let order = NewOrderRequest::market_buy("BTC-PERPETUAL".to_string(), 1.0);
assert_eq!(order.instrument_name, "BTC-PERPETUAL");
assert_eq!(order.amount, 1.0);
assert_eq!(order.order_type, OrderType::Market);
assert_eq!(order.side, OrderSide::Buy);
assert_eq!(order.price, None);
assert_eq!(order.time_in_force, TimeInForce::ImmediateOrCancel);
}
#[test]
fn test_new_order_request_limit_sell() {
let order = NewOrderRequest::limit_sell("ETH-PERPETUAL".to_string(), 2.0, 3500.0);
assert_eq!(order.instrument_name, "ETH-PERPETUAL");
assert_eq!(order.amount, 2.0);
assert_eq!(order.order_type, OrderType::Limit);
assert_eq!(order.side, OrderSide::Sell);
assert_eq!(order.price, Some(3500.0));
assert_eq!(order.time_in_force, TimeInForce::GoodTilCancelled);
}
#[test]
fn test_new_order_request_builder_pattern() {
let order = NewOrderRequest::limit_buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
.with_post_only(true)
.with_reduce_only(false)
.with_label("test_order".to_string())
.with_client_order_id("CLIENT_123".to_string());
assert_eq!(order.post_only, Some(true));
assert_eq!(order.reduce_only, Some(false));
assert_eq!(order.label, Some("test_order".to_string()));
assert_eq!(order.client_order_id, Some("CLIENT_123".to_string()));
}
#[test]
fn test_serialization_roundtrip() {
let order = NewOrderRequest::limit_buy("BTC-PERPETUAL".to_string(), 1.0, 50000.0)
.with_post_only(true)
.with_label("test_order".to_string());
let json = serde_json::to_string(&order).unwrap();
let deserialized: NewOrderRequest = serde_json::from_str(&json).unwrap();
assert_eq!(order.instrument_name, deserialized.instrument_name);
assert_eq!(order.amount, deserialized.amount);
assert_eq!(order.order_type, deserialized.order_type);
assert_eq!(order.side, deserialized.side);
assert_eq!(order.price, deserialized.price);
assert_eq!(order.post_only, deserialized.post_only);
assert_eq!(order.label, deserialized.label);
}
}