1use super::order_types::*;
2use crate::{
3 symbology::{ExecutionVenue, TradableProduct},
4 AccountId, Dir, OrderId, UserId,
5};
6use chrono::{DateTime, Utc};
7use derive_more::{Display, FromStr};
8use rust_decimal::Decimal;
9use schemars::{JsonSchema, JsonSchema_repr};
10use serde::{Deserialize, Serialize};
11use serde_repr::{Deserialize_repr, Serialize_repr};
12use serde_with::skip_serializing_none;
13use strum::{FromRepr, IntoStaticStr};
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
17pub struct Order {
19 pub id: OrderId,
20 #[serde(rename = "pid")]
21 #[schemars(title = "parent_id")]
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub parent_id: Option<OrderId>,
24 #[serde(rename = "eid")]
25 #[schemars(title = "exchange_order_id")]
26 pub exchange_order_id: Option<String>,
27 #[serde(rename = "ts")]
28 #[schemars(title = "recv_time")]
29 pub recv_time: i64,
30 #[serde(rename = "tn")]
31 #[schemars(title = "recv_time_ns")]
32 pub recv_time_ns: u32,
33 #[serde(rename = "o")]
34 #[schemars(title = "status")]
35 pub status: OrderStatus,
36 #[serde(rename = "r", skip_serializing_if = "Option::is_none")]
37 #[schemars(title = "reject_reason")]
38 pub reject_reason: Option<OrderRejectReason>,
39 #[serde(rename = "rm", skip_serializing_if = "Option::is_none")]
40 #[schemars(title = "reject_message")]
41 pub reject_message: Option<String>,
42 #[serde(rename = "s")]
43 #[schemars(title = "symbol")]
44 pub symbol: TradableProduct,
45 #[serde(rename = "u")]
46 #[schemars(title = "trader")]
47 pub trader: UserId,
48 #[serde(rename = "a")]
49 #[schemars(title = "account")]
50 pub account: AccountId,
51 #[serde(rename = "d")]
52 #[schemars(title = "dir")]
53 pub dir: Dir,
54 #[serde(rename = "q")]
55 #[schemars(title = "quantity")]
56 pub quantity: Decimal,
57 #[serde(rename = "xq")]
58 #[schemars(title = "filled_quantity")]
59 pub filled_quantity: Decimal,
60 #[serde(rename = "xp")]
61 #[schemars(title = "average_fill_price")]
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub average_fill_price: Option<Decimal>,
64 #[serde(flatten)]
65 pub order_type: OrderType,
66 #[serde(rename = "tif")]
67 #[schemars(title = "time_in_force")]
68 pub time_in_force: TimeInForce,
69 #[serde(rename = "src")]
70 #[schemars(title = "source")]
71 pub source: OrderSource,
72 #[serde(rename = "ve")]
73 #[schemars(title = "execution_venue")]
74 pub execution_venue: ExecutionVenue,
75 #[serde(rename = "ss", skip_serializing_if = "Option::is_none")]
76 #[schemars(title = "is_short_sale")]
77 pub is_short_sale: Option<bool>,
78}
79
80impl Order {
81 pub fn recv_time(&self) -> Option<DateTime<Utc>> {
82 DateTime::from_timestamp(self.recv_time, self.recv_time_ns)
83 }
84}
85
86#[skip_serializing_none]
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct OrderUpdate {
89 pub id: OrderId,
90 #[serde(rename = "ts")]
91 pub timestamp: i64,
92 #[serde(rename = "tn")]
93 pub timestamp_ns: u32,
94 #[serde(rename = "o")]
95 pub status: Option<OrderStatus>,
96 #[serde(rename = "r")]
97 pub reject_reason: Option<OrderRejectReason>,
98 #[serde(rename = "rm")]
99 pub reject_message: Option<String>,
100 #[serde(rename = "xq")]
101 pub filled_quantity: Option<Decimal>,
102 #[serde(rename = "xp")]
103 pub average_fill_price: Option<Decimal>,
104}
105
106impl OrderUpdate {
107 pub fn timestamp(&self) -> Option<DateTime<Utc>> {
108 DateTime::from_timestamp(self.timestamp, self.timestamp_ns)
109 }
110}
111
112#[derive(
113 Debug, Clone, Copy, IntoStaticStr, Serialize, Deserialize, PartialEq, Eq, JsonSchema,
114)]
115pub enum TimeInForce {
116 #[serde(rename = "GTC")]
117 #[strum(serialize = "GTC")]
118 #[schemars(title = "GoodTilCancel")]
119 GoodTilCancel,
120 #[serde(rename = "GTD")]
121 #[strum(serialize = "GTD")]
122 #[schemars(title = "GoodTilDate")]
123 GoodTilDate(DateTime<Utc>),
124 #[serde(rename = "DAY")]
127 #[strum(serialize = "DAY")]
128 #[schemars(title = "GoodTilDay")]
129 GoodTilDay,
130 #[serde(rename = "IOC")]
131 #[strum(serialize = "IOC")]
132 #[schemars(title = "ImmediateOrCancel")]
133 ImmediateOrCancel,
134 #[serde(rename = "FOK")]
135 #[strum(serialize = "FOK")]
136 #[schemars(title = "FillOrKill")]
137 FillOrKill,
138 #[serde(rename = "ATO")]
139 #[strum(serialize = "ATO")]
140 #[schemars(title = "AtTheOpen")]
141 AtTheOpen,
142 #[serde(rename = "ATC")]
143 #[strum(serialize = "ATC")]
144 #[schemars(title = "AtTheClose")]
145 AtTheClose,
146}
147
148impl TimeInForce {
149 pub fn good_til_date(&self) -> Option<DateTime<Utc>> {
150 match self {
151 Self::GoodTilDate(dt) => Some(*dt),
152 _ => None,
153 }
154 }
155}
156
157#[derive(
158 Debug,
159 Display,
160 FromStr,
161 Clone,
162 Copy,
163 Serialize_repr,
164 Deserialize_repr,
165 PartialEq,
166 Eq,
167 JsonSchema_repr,
168)]
169#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
170#[repr(u8)]
171pub enum OrderSource {
172 API = 0,
173 GUI = 1,
174 Algo = 2,
175 Reconciled = 3,
176 CLI = 4,
177 Telegram = 5,
178 #[serde(other)]
179 Other = 255,
180}
181
182#[cfg(feature = "postgres")]
183crate::to_sql_display!(OrderSource);
184
185#[derive(
186 Debug,
187 Display,
188 FromStr,
189 FromRepr,
190 Clone,
191 Copy,
192 Serialize_repr,
193 Deserialize_repr,
194 PartialEq,
195 Eq,
196 JsonSchema_repr,
197)]
198#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
199#[repr(u8)]
200pub enum OrderStatus {
201 Pending = 0,
202 Open = 1,
203 Rejected = 2,
204 Out = 127,
205 Canceling = 128,
206 Canceled = 129,
207 ReconciledOut = 130,
208 Stale = 254,
209 Unknown = 255,
210}
211
212impl OrderStatus {
213 pub fn is_alive(&self) -> bool {
214 match self {
215 Self::Pending
216 | Self::Open
217 | Self::Canceling
218 | Self::Stale
219 | Self::Unknown => true,
220 Self::Out | Self::Canceled | Self::Rejected | Self::ReconciledOut => false,
221 }
222 }
223
224 pub fn is_dead(&self) -> bool {
225 !self.is_alive()
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
230#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
231pub struct OrderAck {
232 #[serde(rename = "id")]
233 #[schemars(title = "order_id")]
234 pub order_id: OrderId,
235 #[serde(rename = "eid")]
236 #[schemars(title = "exchange_order_id")]
237 pub exchange_order_id: Option<String>,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
241pub struct OrderReject {
242 #[serde(rename = "id")]
243 pub order_id: OrderId,
244 #[serde(rename = "r")]
245 #[schemars(title = "reject_reason")]
246 pub reason: OrderRejectReason,
247 #[serde(rename = "rm", skip_serializing_if = "Option::is_none")]
248 #[schemars(title = "message")]
249 pub message: Option<String>,
250}
251
252impl OrderReject {
253 pub fn to_error_string(&self) -> String {
254 format!(
255 "order {} rejected: {} ({})",
256 self.order_id,
257 self.message.as_deref().unwrap_or("--"),
258 self.reason
259 )
260 }
261}
262
263#[derive(Debug, Display, FromStr, Clone, Copy, Serialize, Deserialize, JsonSchema)]
264#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
265#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT"))]
266pub enum OrderRejectReason {
267 DuplicateOrderId,
268 NotAuthorized,
269 NoExecutionVenue,
270 NoAccount,
271 NoCpty,
272 UnsupportedOrderType,
273 UnsupportedExecutionVenue,
274 InsufficientCash,
275 InsufficientMargin,
276 NotEasyToBorrow,
277 #[serde(other)]
278 Unknown,
279}
280
281#[cfg(feature = "postgres")]
282crate::to_sql_display!(OrderRejectReason);
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
285#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
286pub struct OrderCanceling {
287 #[serde(rename = "id")]
288 pub order_id: OrderId,
289 #[serde(rename = "xid", skip_serializing_if = "Option::is_none")]
290 pub cancel_id: Option<Uuid>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
294#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
295pub struct OrderCanceled {
296 #[serde(rename = "id")]
297 pub order_id: OrderId,
298 #[serde(rename = "xid", skip_serializing_if = "Option::is_none")]
299 pub cancel_id: Option<Uuid>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
303#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
304pub struct OrderOut {
305 #[serde(rename = "id")]
306 pub order_id: OrderId,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
310#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
311pub struct OrderStale {
312 #[serde(rename = "id")]
313 pub order_id: OrderId,
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{oms::PlaceOrderRequest, AccountIdOrName, AccountName, TraderIdOrEmail};
320 use rust_decimal_macros::dec;
321
322 #[test]
323 fn test_place_order_request_json() {
324 #[rustfmt::skip]
325 let por: PlaceOrderRequest = serde_json::from_str(r#"
326 {
327 "id": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:123",
328 "s": "BTC Crypto/USD",
329 "d": "BUY",
330 "q": "100",
331 "u": "trader1",
332 "a": "COINBASE:TEST",
333 "k": "LIMIT",
334 "p": "4500",
335 "po": true,
336 "tif": {
337 "GTD": "2025-01-05T04:20:00Z"
338 }
339 }
340 "#).unwrap();
341 let trader: UserId = "trader1".parse().unwrap();
342 assert_eq!(
343 por,
344 PlaceOrderRequest {
345 id: Some(OrderId {
346 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
347 seqno: 123
348 }),
349 parent_id: None,
350 symbol: "BTC Crypto/USD".into(),
351 dir: Dir::Buy,
352 quantity: dec!(100),
353 trader: Some(TraderIdOrEmail::Id(trader)),
354 account: Some(AccountIdOrName::Name(
355 AccountName::new("COINBASE", "TEST").unwrap()
356 )),
357 order_type: OrderType::Limit(LimitOrderType {
358 limit_price: dec!(4500),
359 post_only: true,
360 }),
361 time_in_force: TimeInForce::GoodTilDate(
362 "2025-01-05T04:20:00Z".parse().unwrap()
363 ),
364 source: None,
365 execution_venue: None,
366 }
367 );
368 }
369
370 #[test]
371 fn test_order_json() {
372 let recv_time: DateTime<Utc> = "2025-01-01T04:20:00Z".parse().unwrap();
373 insta::assert_json_snapshot!(Order {
374 id: OrderId {
375 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
376 seqno: 123
377 },
378 parent_id: None,
379 exchange_order_id: None,
380 recv_time: recv_time.timestamp(),
381 recv_time_ns: recv_time.timestamp_subsec_nanos(),
382 status: OrderStatus::Out,
383 reject_reason: Some(OrderRejectReason::DuplicateOrderId),
384 reject_message: None,
385 symbol: "BTC Crypto/USD".parse().unwrap(),
386 trader: UserId::anonymous(),
387 account: AccountId::nil(),
388 dir: Dir::Buy,
389 quantity: dec!(100),
390 filled_quantity: dec!(0),
391 average_fill_price: None,
392 order_type: OrderType::Limit(LimitOrderType {
393 limit_price: dec!(4500),
394 post_only: false,
395 }),
396 time_in_force: TimeInForce::GoodTilCancel,
397 source: OrderSource::API,
398 execution_venue: "BINANCE".into(),
399 is_short_sale: None,
400 }, @r###"
401 {
402 "id": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:123",
403 "eid": null,
404 "ts": 1735705200,
405 "tn": 0,
406 "o": 127,
407 "r": "DuplicateOrderId",
408 "s": "BTC Crypto/USD",
409 "u": "00000000-0000-0000-0000-000000000000",
410 "a": "00000000-0000-0000-0000-000000000000",
411 "d": "BUY",
412 "q": "100",
413 "xq": "0",
414 "k": "LIMIT",
415 "p": "4500",
416 "po": false,
417 "tif": "GTC",
418 "src": 0,
419 "ve": "BINANCE"
420 }
421 "###);
422 let recv_time: DateTime<Utc> = "2025-01-01T04:20:00Z".parse().unwrap();
423 insta::assert_json_snapshot!(Order {
424 id: OrderId::nil(123),
425 parent_id: Some(OrderId {
426 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
427 seqno: 456
428 }),
429 exchange_order_id: None,
430 recv_time: recv_time.timestamp(),
431 recv_time_ns: recv_time.timestamp_subsec_nanos(),
432 status: OrderStatus::Open,
433 reject_reason: None,
434 reject_message: None,
435 symbol: "ETH Crypto/USD".parse().unwrap(),
436 trader: UserId::anonymous(),
437 account: AccountId::nil(),
438 dir: Dir::Sell,
439 quantity: dec!(0.7050),
440 filled_quantity: dec!(0.7050),
441 average_fill_price: Some(dec!(4250)),
442 order_type: OrderType::StopLossLimit(StopLossLimitOrderType {
443 limit_price: dec!(4500),
444 trigger_price: dec!(4000),
445 }),
446 time_in_force: TimeInForce::GoodTilDate(
447 "2025-01-05T04:20:00Z".parse().unwrap()
448 ),
449 source: OrderSource::Telegram,
450 execution_venue: "BINANCE".into(),
451 is_short_sale: None,
452 }, @r###"
453 {
454 "id": "123",
455 "pid": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:456",
456 "eid": null,
457 "ts": 1735705200,
458 "tn": 0,
459 "o": 1,
460 "s": "ETH Crypto/USD",
461 "u": "00000000-0000-0000-0000-000000000000",
462 "a": "00000000-0000-0000-0000-000000000000",
463 "d": "SELL",
464 "q": "0.7050",
465 "xq": "0.7050",
466 "xp": "4250",
467 "k": "STOP_LOSS_LIMIT",
468 "p": "4500",
469 "tp": "4000",
470 "tif": {
471 "GTD": "2025-01-05T04:20:00Z"
472 },
473 "src": 5,
474 "ve": "BINANCE"
475 }
476 "###);
477 }
478}