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