Skip to main content

mkt_types/
trading.rs

1use derive_builder::Builder;
2use rust_decimal::Decimal;
3use strum_macros::{Display, EnumString};
4use time::OffsetDateTime;
5
6use crate::{Extensions, MarketKind, Symbol};
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[non_exhaustive]
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct OrderId(pub String);
12
13impl OrderId {
14    pub fn new(value: impl Into<String>) -> Self {
15        Self(value.into())
16    }
17}
18
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[non_exhaustive]
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct ClientOrderId(pub String);
23
24impl ClientOrderId {
25    pub fn new(value: impl Into<String>) -> Self {
26        Self(value.into())
27    }
28}
29
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[non_exhaustive]
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub enum OrderKey {
34    Exchange(OrderId),
35    Client(ClientOrderId),
36}
37
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[non_exhaustive]
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
41#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
42pub enum OrderSide {
43    Buy,
44    Sell,
45}
46
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[non_exhaustive]
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
50#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
51pub enum OrderType {
52    Market,
53    Limit,
54    StopMarket,
55    StopLimit,
56    PostOnly,
57}
58
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[non_exhaustive]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
62#[strum(serialize_all = "SCREAMING_SNAKE_CASE", ascii_case_insensitive)]
63pub enum TimeInForce {
64    Gtc,
65    Ioc,
66    Fok,
67    Gtx,
68}
69
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[non_exhaustive]
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
73#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
74pub enum OrderStatus {
75    New,
76    PartiallyFilled,
77    Filled,
78    Canceled,
79    Rejected,
80    Expired,
81}
82
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84#[non_exhaustive]
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
86#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
87pub enum PositionSide {
88    Long,
89    Short,
90    Both,
91}
92
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94#[non_exhaustive]
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
96#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
97pub enum MarginMode {
98    Cross,
99    Isolated,
100}
101
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[non_exhaustive]
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub enum OrderQuantity {
106    Base(Decimal),
107    Quote(Decimal),
108}
109
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111#[non_exhaustive]
112#[derive(Debug, Clone, PartialEq, Builder)]
113#[builder(pattern = "owned", setter(into))]
114#[builder(build_fn(validate = "Self::validate"))]
115pub struct SpotOrderRequest {
116    pub symbol: Symbol,
117    pub side: OrderSide,
118    pub order_type: OrderType,
119    pub quantity: OrderQuantity,
120    #[builder(default)]
121    pub price: Option<Decimal>,
122    #[builder(default)]
123    pub time_in_force: Option<TimeInForce>,
124    #[builder(default)]
125    pub client_order_id: Option<ClientOrderId>,
126    #[builder(default)]
127    pub extensions: Extensions,
128}
129
130impl SpotOrderRequest {
131    pub fn builder() -> SpotOrderRequestBuilder {
132        SpotOrderRequestBuilder::default()
133    }
134}
135
136impl SpotOrderRequestBuilder {
137    fn validate(&self) -> Result<(), String> {
138        let Some(quantity) = self.quantity else {
139            return Err("quantity is required".to_owned());
140        };
141
142        match quantity {
143            OrderQuantity::Base(value) if value <= Decimal::ZERO => {
144                Err("quantity must be greater than zero".to_owned())
145            }
146            OrderQuantity::Quote(value) if value <= Decimal::ZERO => {
147                Err("quote quantity must be greater than zero".to_owned())
148            }
149            _ => Ok(()),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::{OrderQuantity, OrderSide, OrderType, SpotOrderRequest};
157    use crate::Symbol;
158    use rust_decimal::Decimal;
159
160    #[test]
161    fn spot_order_request_builder_requires_positive_quantity_mode() {
162        let missing = SpotOrderRequest::builder()
163            .symbol(Symbol::spot("BTCUSDT"))
164            .side(OrderSide::Buy)
165            .order_type(OrderType::Market)
166            .build()
167            .expect_err("missing quantity must be rejected");
168        assert!(missing.to_string().contains("quantity is required"));
169
170        let zero_base = SpotOrderRequest::builder()
171            .symbol(Symbol::spot("BTCUSDT"))
172            .side(OrderSide::Buy)
173            .order_type(OrderType::Market)
174            .quantity(OrderQuantity::Base(Decimal::ZERO))
175            .build()
176            .expect_err("zero base quantity must be rejected");
177        assert!(zero_base
178            .to_string()
179            .contains("quantity must be greater than zero"));
180
181        let zero_quote = SpotOrderRequest::builder()
182            .symbol(Symbol::spot("BTCUSDT"))
183            .side(OrderSide::Buy)
184            .order_type(OrderType::Market)
185            .quantity(OrderQuantity::Quote(Decimal::ZERO))
186            .build()
187            .expect_err("zero quote quantity must be rejected");
188        assert!(zero_quote
189            .to_string()
190            .contains("quote quantity must be greater than zero"));
191    }
192}
193
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195#[non_exhaustive]
196#[derive(Debug, Clone, PartialEq, Builder)]
197#[builder(pattern = "owned", setter(into))]
198pub struct FuturesOrderRequest {
199    pub symbol: Symbol,
200    pub side: OrderSide,
201    pub order_type: OrderType,
202    pub quantity: Decimal,
203    #[builder(default)]
204    pub price: Option<Decimal>,
205    #[builder(default)]
206    pub time_in_force: Option<TimeInForce>,
207    #[builder(default)]
208    pub position_side: Option<PositionSide>,
209    #[builder(default)]
210    pub reduce_only: bool,
211    #[builder(default)]
212    pub client_order_id: Option<ClientOrderId>,
213    #[builder(default)]
214    pub extensions: Extensions,
215}
216
217impl FuturesOrderRequest {
218    pub fn builder() -> FuturesOrderRequestBuilder {
219        FuturesOrderRequestBuilder::default()
220    }
221}
222
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224#[non_exhaustive]
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct SpotCancelOrderRequest {
227    pub symbol: Symbol,
228    pub key: OrderKey,
229}
230
231impl SpotCancelOrderRequest {
232    pub fn new(symbol: Symbol, key: OrderKey) -> Self {
233        Self { symbol, key }
234    }
235}
236
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238#[non_exhaustive]
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct FuturesCancelOrderRequest {
241    pub symbol: Symbol,
242    pub key: OrderKey,
243}
244
245impl FuturesCancelOrderRequest {
246    pub fn new(symbol: Symbol, key: OrderKey) -> Self {
247        Self { symbol, key }
248    }
249}
250
251#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
252#[non_exhaustive]
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub struct SpotOrderQuery {
255    pub symbol: Symbol,
256    pub key: OrderKey,
257}
258
259impl SpotOrderQuery {
260    pub fn new(symbol: Symbol, key: OrderKey) -> Self {
261        Self { symbol, key }
262    }
263}
264
265#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
266#[non_exhaustive]
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct FuturesOrderQuery {
269    pub symbol: Symbol,
270    pub key: OrderKey,
271}
272
273impl FuturesOrderQuery {
274    pub fn new(symbol: Symbol, key: OrderKey) -> Self {
275        Self { symbol, key }
276    }
277}
278
279#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
280#[non_exhaustive]
281#[derive(Debug, Clone, PartialEq, Builder)]
282#[builder(pattern = "owned", setter(into))]
283pub struct Order {
284    pub id: OrderId,
285    #[builder(default)]
286    pub client_order_id: Option<ClientOrderId>,
287    pub symbol: Symbol,
288    pub market_kind: MarketKind,
289    pub side: OrderSide,
290    pub order_type: OrderType,
291    pub status: OrderStatus,
292    #[builder(default)]
293    pub time_in_force: Option<TimeInForce>,
294    #[builder(default)]
295    pub price: Option<Decimal>,
296    #[builder(default)]
297    pub average_price: Option<Decimal>,
298    pub quantity: Decimal,
299    pub filled_quantity: Decimal,
300    #[builder(default)]
301    pub original_quote_quantity: Option<Decimal>,
302    #[builder(default)]
303    pub cumulative_quote_quantity: Option<Decimal>,
304    #[cfg_attr(
305        feature = "serde",
306        serde(with = "time::serde::timestamp::milliseconds")
307    )]
308    pub created_at: OffsetDateTime,
309    #[cfg_attr(
310        feature = "serde",
311        serde(with = "time::serde::timestamp::milliseconds::option")
312    )]
313    #[builder(default)]
314    pub updated_at: Option<OffsetDateTime>,
315    #[builder(default)]
316    pub extensions: Extensions,
317}
318
319impl Order {
320    pub fn builder() -> OrderBuilder {
321        OrderBuilder::default()
322    }
323}
324
325#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
326#[non_exhaustive]
327#[derive(Debug, Clone, PartialEq, Builder)]
328#[builder(pattern = "owned", setter(into))]
329pub struct Fill {
330    #[builder(default)]
331    pub id: Option<String>,
332    pub order_id: OrderId,
333    pub symbol: Symbol,
334    pub side: OrderSide,
335    pub price: Decimal,
336    pub quantity: Decimal,
337    #[builder(default)]
338    pub quote_quantity: Option<Decimal>,
339    #[builder(default)]
340    pub fee: Option<Decimal>,
341    #[builder(default)]
342    pub fee_asset: Option<String>,
343    #[cfg_attr(
344        feature = "serde",
345        serde(with = "time::serde::timestamp::milliseconds")
346    )]
347    pub timestamp: OffsetDateTime,
348    #[builder(default)]
349    pub extensions: Extensions,
350}
351
352impl Fill {
353    pub fn builder() -> FillBuilder {
354        FillBuilder::default()
355    }
356}
357
358#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
359#[non_exhaustive]
360#[derive(Debug, Clone, PartialEq, Builder)]
361#[builder(pattern = "owned", setter(into))]
362pub struct SetLeverageRequest {
363    pub symbol: Symbol,
364    pub leverage: Decimal,
365    #[builder(default)]
366    pub margin_mode: Option<MarginMode>,
367    #[builder(default)]
368    pub position_side: Option<PositionSide>,
369}
370
371impl SetLeverageRequest {
372    pub fn builder() -> SetLeverageRequestBuilder {
373        SetLeverageRequestBuilder::default()
374    }
375}