use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseTrade {
#[serde(default)]
pub trade_seq: Option<u64>,
#[serde(default)]
pub trade_id: Option<String>,
#[serde(default)]
pub timestamp: Option<u64>,
#[serde(default)]
pub tick_direction: Option<i32>,
#[serde(default)]
pub state: Option<String>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub price: Option<f64>,
#[serde(default)]
pub post_only: Option<bool>,
#[serde(default)]
pub order_type: Option<String>,
#[serde(default)]
pub order_id: Option<String>,
#[serde(default)]
pub matching_id: Option<String>,
#[serde(default)]
pub mark_price: Option<f64>,
#[serde(default)]
pub liquidity: Option<String>,
#[serde(default)]
pub instrument_name: Option<String>,
#[serde(default)]
pub index_price: Option<f64>,
#[serde(default)]
pub fee_currency: Option<String>,
#[serde(default)]
pub fee: Option<f64>,
#[serde(default)]
pub direction: Option<String>,
#[serde(default)]
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CloseOrder {
#[serde(default)]
pub web: Option<bool>,
#[serde(default)]
pub time_in_force: Option<String>,
#[serde(default)]
pub replaced: Option<bool>,
#[serde(default)]
pub reduce_only: Option<bool>,
#[serde(default)]
pub price: Option<f64>,
#[serde(default)]
pub post_only: Option<bool>,
#[serde(default)]
pub order_type: Option<String>,
#[serde(default)]
pub order_state: Option<String>,
#[serde(default)]
pub order_id: Option<String>,
#[serde(default)]
pub max_show: Option<f64>,
#[serde(default)]
pub last_update_timestamp: Option<u64>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub is_rebalance: Option<bool>,
#[serde(default)]
pub is_liquidation: Option<bool>,
#[serde(default)]
pub instrument_name: Option<String>,
#[serde(default)]
pub filled_amount: Option<f64>,
#[serde(default)]
pub direction: Option<String>,
#[serde(default)]
pub creation_timestamp: Option<u64>,
#[serde(default)]
pub average_price: Option<f64>,
#[serde(default)]
pub api: Option<bool>,
#[serde(default)]
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClosePositionResponse {
#[serde(default)]
pub trades: Vec<CloseTrade>,
#[serde(default)]
pub order: Option<CloseOrder>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MovePositionTrade {
pub instrument_name: String,
pub amount: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub price: Option<f64>,
}
impl MovePositionTrade {
#[must_use]
pub fn new(instrument_name: &str, amount: f64) -> Self {
Self {
instrument_name: instrument_name.to_string(),
amount,
price: None,
}
}
#[must_use]
pub fn with_price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MovePositionResult {
pub target_uid: u64,
pub source_uid: u64,
pub price: f64,
pub instrument_name: String,
pub direction: String,
pub amount: f64,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_move_position_trade_new() {
let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
assert_eq!(trade.amount, 100.0);
assert!(trade.price.is_none());
}
#[test]
fn test_move_position_trade_with_price() {
let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0).with_price(50000.0);
assert_eq!(trade.instrument_name, "BTC-PERPETUAL");
assert_eq!(trade.amount, 100.0);
assert_eq!(trade.price, Some(50000.0));
}
#[test]
fn test_move_position_trade_serialization() {
let trade = MovePositionTrade::new("ETH-PERPETUAL", 50.0).with_price(3000.0);
let json = serde_json::to_string(&trade).expect("serialize");
assert!(json.contains("ETH-PERPETUAL"));
assert!(json.contains("50"));
assert!(json.contains("3000"));
}
#[test]
fn test_move_position_trade_without_price_serialization() {
let trade = MovePositionTrade::new("BTC-PERPETUAL", 100.0);
let json = serde_json::to_string(&trade).expect("serialize");
assert!(!json.contains("price"));
}
#[test]
fn test_move_position_result_deserialization() {
let json = r#"{
"target_uid": 23,
"source_uid": 3,
"price": 35800.0,
"instrument_name": "BTC-PERPETUAL",
"direction": "buy",
"amount": 110.0
}"#;
let result: MovePositionResult = serde_json::from_str(json).expect("deserialize");
assert_eq!(result.target_uid, 23);
assert_eq!(result.source_uid, 3);
assert_eq!(result.price, 35800.0);
assert_eq!(result.instrument_name, "BTC-PERPETUAL");
assert_eq!(result.direction, "buy");
assert_eq!(result.amount, 110.0);
}
#[test]
fn test_close_position_response_deserialization() {
let json = r#"{
"trades": [{
"trade_seq": 1966068,
"trade_id": "ETH-2696097",
"timestamp": 1590486335742,
"tick_direction": 0,
"state": "filled",
"reduce_only": true,
"price": 202.8,
"post_only": false,
"order_type": "limit",
"order_id": "ETH-584864807",
"mark_price": 202.79,
"liquidity": "T",
"instrument_name": "ETH-PERPETUAL",
"index_price": 202.86,
"fee_currency": "ETH",
"fee": 0.00007766,
"direction": "sell",
"amount": 21.0
}],
"order": {
"time_in_force": "good_til_cancelled",
"reduce_only": true,
"price": 198.75,
"post_only": false,
"order_type": "limit",
"order_state": "filled",
"order_id": "ETH-584864807",
"instrument_name": "ETH-PERPETUAL",
"filled_amount": 21.0,
"direction": "sell",
"creation_timestamp": 1590486335742,
"average_price": 202.8,
"api": true,
"amount": 21.0
}
}"#;
let response: ClosePositionResponse = serde_json::from_str(json).expect("deserialize");
assert_eq!(response.trades.len(), 1);
assert!(response.order.is_some());
let trade = &response.trades[0];
assert_eq!(trade.trade_id, Some("ETH-2696097".to_string()));
assert_eq!(trade.price, Some(202.8));
assert_eq!(trade.direction, Some("sell".to_string()));
let order = response.order.as_ref().expect("order");
assert_eq!(order.order_id, Some("ETH-584864807".to_string()));
assert_eq!(order.order_state, Some("filled".to_string()));
}
#[test]
fn test_close_trade_deserialization() {
let json = r#"{
"trade_seq": 12345,
"trade_id": "BTC-123456",
"price": 50000.0,
"amount": 1.0,
"direction": "buy"
}"#;
let trade: CloseTrade = serde_json::from_str(json).expect("deserialize");
assert_eq!(trade.trade_seq, Some(12345));
assert_eq!(trade.trade_id, Some("BTC-123456".to_string()));
assert_eq!(trade.price, Some(50000.0));
}
#[test]
fn test_close_order_deserialization() {
let json = r#"{
"order_id": "BTC-123",
"order_state": "open",
"order_type": "limit",
"price": 45000.0,
"amount": 100.0,
"direction": "sell"
}"#;
let order: CloseOrder = serde_json::from_str(json).expect("deserialize");
assert_eq!(order.order_id, Some("BTC-123".to_string()));
assert_eq!(order.order_state, Some("open".to_string()));
assert_eq!(order.price, Some(45000.0));
}
}