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_json::json;
12use serde_repr::{Deserialize_repr, Serialize_repr};
13use serde_with::skip_serializing_none;
14use strum::{FromRepr, IntoStaticStr};
15use uuid::Uuid;
16
17#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
18pub 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 pub reason: OrderRejectReason,
246 #[serde(rename = "rm", skip_serializing_if = "Option::is_none")]
247 pub message: Option<String>,
248}
249
250impl OrderReject {
251 pub fn to_error_string(&self) -> String {
252 format!(
253 "order {} rejected: {} ({})",
254 self.order_id,
255 self.message.as_deref().unwrap_or("--"),
256 self.reason
257 )
258 }
259}
260
261#[derive(Debug, Display, FromStr, Clone, Copy, Serialize, Deserialize, JsonSchema)]
262#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
263#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT"))]
264pub enum OrderRejectReason {
265 DuplicateOrderId,
266 NotAuthorized,
267 NoExecutionVenue,
268 NoAccount,
269 NoCpty,
270 UnsupportedOrderType,
271 UnsupportedExecutionVenue,
272 InsufficientCash,
273 InsufficientMargin,
274 NotEasyToBorrow,
275 #[serde(other)]
276 Unknown,
277}
278
279#[cfg(feature = "postgres")]
280crate::to_sql_display!(OrderRejectReason);
281
282#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
283#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
284pub struct OrderCanceling {
285 #[serde(rename = "id")]
286 pub order_id: OrderId,
287 #[serde(rename = "xid", skip_serializing_if = "Option::is_none")]
288 pub cancel_id: Option<Uuid>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
292#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
293pub struct OrderCanceled {
294 #[serde(rename = "id")]
295 pub order_id: OrderId,
296 #[serde(rename = "xid", skip_serializing_if = "Option::is_none")]
297 pub cancel_id: Option<Uuid>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
301#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
302pub struct OrderOut {
303 #[serde(rename = "id")]
304 pub order_id: OrderId,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
308#[cfg_attr(feature = "graphql", derive(juniper::GraphQLObject))]
309pub struct OrderStale {
310 #[serde(rename = "id")]
311 pub order_id: OrderId,
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::{oms::PlaceOrderRequest, AccountIdOrName, AccountName, TraderIdOrEmail};
318 use rust_decimal_macros::dec;
319
320 #[test]
321 fn test_place_order_request_json() {
322 #[rustfmt::skip]
323 let por: PlaceOrderRequest = serde_json::from_str(r#"
324 {
325 "id": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:123",
326 "s": "BTC Crypto/USD",
327 "d": "BUY",
328 "q": "100",
329 "u": "trader1",
330 "a": "COINBASE:TEST",
331 "k": "LIMIT",
332 "p": "4500",
333 "po": true,
334 "tif": {
335 "GTD": "2025-01-05T04:20:00Z"
336 }
337 }
338 "#).unwrap();
339 let trader: UserId = "trader1".parse().unwrap();
340 assert_eq!(
341 por,
342 PlaceOrderRequest {
343 id: Some(OrderId {
344 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
345 seqno: 123
346 }),
347 parent_id: None,
348 symbol: "BTC Crypto/USD".into(),
349 dir: Dir::Buy,
350 quantity: dec!(100),
351 trader: Some(TraderIdOrEmail::Id(trader)),
352 account: Some(AccountIdOrName::Name(
353 AccountName::new("COINBASE", "TEST").unwrap()
354 )),
355 order_type: OrderType::Limit(LimitOrderType {
356 limit_price: dec!(4500),
357 post_only: true,
358 }),
359 time_in_force: TimeInForce::GoodTilDate(
360 "2025-01-05T04:20:00Z".parse().unwrap()
361 ),
362 source: None,
363 execution_venue: None,
364 }
365 );
366 }
367
368 #[test]
369 fn test_order_json() {
370 let recv_time: DateTime<Utc> = "2025-01-01T04:20:00Z".parse().unwrap();
371 insta::assert_json_snapshot!(Order {
372 id: OrderId {
373 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
374 seqno: 123
375 },
376 parent_id: None,
377 exchange_order_id: None,
378 recv_time: recv_time.timestamp(),
379 recv_time_ns: recv_time.timestamp_subsec_nanos(),
380 status: OrderStatus::Out,
381 reject_reason: Some(OrderRejectReason::DuplicateOrderId),
382 reject_message: None,
383 symbol: "BTC Crypto/USD".parse().unwrap(),
384 trader: UserId::anonymous(),
385 account: AccountId::nil(),
386 dir: Dir::Buy,
387 quantity: dec!(100),
388 filled_quantity: dec!(0),
389 average_fill_price: None,
390 order_type: OrderType::Limit(LimitOrderType {
391 limit_price: dec!(4500),
392 post_only: false,
393 }),
394 time_in_force: TimeInForce::GoodTilCancel,
395 source: OrderSource::API,
396 execution_venue: "BINANCE".into(),
397 is_short_sale: None,
398 }, @r###"
399 {
400 "id": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:123",
401 "eid": null,
402 "ts": 1735705200,
403 "tn": 0,
404 "o": 127,
405 "r": "DuplicateOrderId",
406 "s": "BTC Crypto/USD",
407 "u": "00000000-0000-0000-0000-000000000000",
408 "a": "00000000-0000-0000-0000-000000000000",
409 "d": "BUY",
410 "q": "100",
411 "xq": "0",
412 "k": "LIMIT",
413 "p": "4500",
414 "po": false,
415 "tif": "GTC",
416 "src": 0,
417 "ve": "BINANCE"
418 }
419 "###);
420 let recv_time: DateTime<Utc> = "2025-01-01T04:20:00Z".parse().unwrap();
421 insta::assert_json_snapshot!(Order {
422 id: OrderId::nil(123),
423 parent_id: Some(OrderId {
424 seqid: "d3f97244-78e6-4549-abf6-90adfe0ab7fe".parse().unwrap(),
425 seqno: 456
426 }),
427 exchange_order_id: None,
428 recv_time: recv_time.timestamp(),
429 recv_time_ns: recv_time.timestamp_subsec_nanos(),
430 status: OrderStatus::Open,
431 reject_reason: None,
432 reject_message: None,
433 symbol: "ETH Crypto/USD".parse().unwrap(),
434 trader: UserId::anonymous(),
435 account: AccountId::nil(),
436 dir: Dir::Sell,
437 quantity: dec!(0.7050),
438 filled_quantity: dec!(0.7050),
439 average_fill_price: Some(dec!(4250)),
440 order_type: OrderType::StopLossLimit(StopLossLimitOrderType {
441 limit_price: dec!(4500),
442 trigger_price: dec!(4000),
443 }),
444 time_in_force: TimeInForce::GoodTilDate(
445 "2025-01-05T04:20:00Z".parse().unwrap()
446 ),
447 source: OrderSource::Telegram,
448 execution_venue: "BINANCE".into(),
449 is_short_sale: None,
450 }, @r###"
451 {
452 "id": "123",
453 "pid": "d3f97244-78e6-4549-abf6-90adfe0ab7fe:456",
454 "eid": null,
455 "ts": 1735705200,
456 "tn": 0,
457 "o": 1,
458 "s": "ETH Crypto/USD",
459 "u": "00000000-0000-0000-0000-000000000000",
460 "a": "00000000-0000-0000-0000-000000000000",
461 "d": "SELL",
462 "q": "0.7050",
463 "xq": "0.7050",
464 "xp": "4250",
465 "k": "STOP_LOSS_LIMIT",
466 "p": "4500",
467 "tp": "4000",
468 "tif": {
469 "GTD": "2025-01-05T04:20:00Z"
470 },
471 "src": 5,
472 "ve": "BINANCE"
473 }
474 "###);
475 }
476}