architect_api/orderflow/
order.rs

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)]
18/// <!-- py: unflatten=k/order_type/OrderType, tag=k -->
19pub 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    /// Timestamp that the Architect OMS first received the order.
29    ///
30    /// For reconciled orders, this could be very far in the future
31    /// relative to the exchange order timestamp.
32    #[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    /// Day order--the specific time which this expires
136    /// will be dependent on the venue
137    #[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}