architect_api/orderflow/
order.rs

1use crate::{
2    symbology::{MarketId, VenueId},
3    AccountId, Dir, OrderId, Str, UserId,
4};
5use anyhow::{anyhow, Result};
6use arcstr::ArcStr;
7use chrono::{DateTime, Utc};
8use derive_builder::Builder;
9use enumflags2::{bitflags, BitFlags};
10#[cfg(feature = "netidx")]
11use netidx_derive::Pack;
12use rust_decimal::Decimal;
13use schemars::{JsonSchema, JsonSchema_repr};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16
17// #[derive(Builder, Debug, Clone, Copy, Serialize, Deserialize)]
18// #[cfg_attr(feature = "netidx", derive(Pack))]
19// pub struct OrderRequest {
20//     pub id: Option<OrderId>,
21//     pub market: MarketId,
22//     pub dir: Dir,
23//     pub quantity: Decimal,
24//     #[builder(setter(strip_option), default)]
25//     pub trader: Option<UserId>,
26//     #[builder(setter(strip_option), default)]
27//     pub account: AccountSpecifier,
28//     pub order_type: OrderType,
29//     #[builder(default = "TimeInForce::GoodTilCancel")]
30//     pub time_in_force: TimeInForce,
31//     #[builder(setter(strip_option), default)]
32//     pub quote_id: Option<Str>,
33//     pub source: OrderSource,
34//     #[builder(setter(strip_option), default)]
35//     pub parent_order: Option<ParentOrder>,
36// }
37
38#[derive(
39    Builder, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema,
40)]
41#[cfg_attr(feature = "netidx", derive(Pack))]
42pub struct Order {
43    pub id: OrderId,
44    pub market: MarketId,
45    pub dir: Dir,
46    pub quantity: Decimal,
47    #[builder(setter(strip_option), default)]
48    pub trader: Option<UserId>,
49    #[builder(setter(strip_option), default)]
50    pub account: Option<AccountId>,
51    pub order_type: OrderType,
52    #[builder(default = "TimeInForce::GoodTilCancel")]
53    pub time_in_force: TimeInForce,
54    #[builder(setter(strip_option), default)]
55    pub quote_id: Option<Str>,
56    pub source: OrderSource,
57    #[builder(setter(strip_option), default)]
58    pub parent_order: Option<ParentOrder>,
59    // pub recv_time: DateTime<Utc>,
60}
61
62impl Order {
63    pub fn limit_price(&self) -> Decimal {
64        self.order_type.limit_price()
65    }
66}
67
68impl PartialOrd for Order {
69    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
70        self.id.partial_cmp(&other.id)
71    }
72}
73
74impl Ord for Order {
75    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
76        self.id.cmp(&other.id)
77    }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema_repr)]
81#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
82#[cfg_attr(feature = "netidx", derive(Pack))]
83#[serde(rename_all = "snake_case")]
84#[repr(u8)]
85pub enum OrderSource {
86    #[serde(rename = "api")]
87    API,
88    #[serde(rename = "gui")]
89    GUI,
90    Algo,
91    External,
92    #[serde(rename = "cli")]
93    CLI,
94    Telegram,
95    #[serde(other)]
96    #[cfg_attr(feature = "netidx", pack(other))]
97    Other,
98}
99
100#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema_repr)]
101#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
102#[cfg_attr(feature = "netidx", derive(Pack))]
103#[repr(u8)]
104pub enum ParentOrderKind {
105    Algo,
106    Order,
107}
108
109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
110#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
111#[cfg_attr(feature = "netidx", derive(Pack))]
112pub struct ParentOrder {
113    pub kind: ParentOrderKind,
114    pub id: OrderId,
115}
116
117impl ParentOrder {
118    pub fn new(kind: ParentOrderKind, id: OrderId) -> Self {
119        Self { kind, id }
120    }
121}
122
123#[cfg(feature = "netidx")]
124impl OrderBuilder {
125    pub fn new(order_id: OrderId, source: OrderSource, market: MarketId) -> Self {
126        let mut t = Self::default();
127        t.id(order_id);
128        t.source(source);
129        t.market(market);
130        t
131    }
132
133    /// Option version of trader(&mut self, ..)
134    pub fn with_trader(&mut self, trader: Option<UserId>) -> &mut Self {
135        self.trader = Some(trader);
136        self
137    }
138
139    /// Option version of account(&mut self, ..)
140    pub fn with_account(&mut self, account: Option<AccountId>) -> &mut Self {
141        self.account = Some(account);
142        self
143    }
144
145    pub fn limit(
146        &mut self,
147        dir: Dir,
148        quantity: Decimal,
149        limit_price: Decimal,
150        post_only: bool,
151    ) -> &mut Self {
152        self.dir(dir);
153        self.quantity(quantity);
154        self.order_type(OrderType::Limit(LimitOrderType { limit_price, post_only }));
155        self
156    }
157
158    pub fn stop_loss_limit(
159        &mut self,
160        dir: Dir,
161        quantity: Decimal,
162        limit_price: Decimal,
163        trigger_price: Decimal,
164    ) -> &mut Self {
165        self.dir(dir);
166        self.quantity(quantity);
167        self.order_type(OrderType::StopLossLimit(StopLossLimitOrderType {
168            limit_price,
169            trigger_price,
170        }));
171        self
172    }
173
174    pub fn take_profit_limit(
175        &mut self,
176        dir: Dir,
177        quantity: Decimal,
178        limit_price: Decimal,
179        trigger_price: Decimal,
180    ) -> &mut Self {
181        self.dir(dir);
182        self.quantity(quantity);
183        self.order_type(OrderType::TakeProfitLimit(TakeProfitLimitOrderType {
184            limit_price,
185            trigger_price,
186        }));
187        self
188    }
189}
190
191#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
192#[cfg_attr(feature = "juniper", derive(juniper::GraphQLUnion))]
193#[cfg_attr(feature = "netidx", derive(Pack))]
194#[serde(tag = "type")]
195pub enum OrderType {
196    Limit(LimitOrderType),
197    StopLossLimit(StopLossLimitOrderType),
198    TakeProfitLimit(TakeProfitLimitOrderType),
199}
200
201impl OrderType {
202    pub fn limit_price(&self) -> Decimal {
203        match self {
204            OrderType::Limit(limit_order_type) => limit_order_type.limit_price,
205            OrderType::StopLossLimit(stop_loss_limit_order_type) => {
206                stop_loss_limit_order_type.limit_price
207            }
208            OrderType::TakeProfitLimit(take_profit_limit_order_type) => {
209                take_profit_limit_order_type.limit_price
210            }
211        }
212    }
213
214    pub fn post_only(&self) -> Option<bool> {
215        match self {
216            OrderType::Limit(limit_order_type) => Some(limit_order_type.post_only),
217            OrderType::StopLossLimit(_) | OrderType::TakeProfitLimit(_) => None,
218        }
219    }
220
221    pub fn trigger_price(&self) -> Option<Decimal> {
222        match self {
223            OrderType::StopLossLimit(sllot) => Some(sllot.trigger_price),
224            OrderType::TakeProfitLimit(tplot) => Some(tplot.trigger_price),
225            OrderType::Limit(_) => None,
226        }
227    }
228}
229
230#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
231#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
232#[cfg_attr(feature = "netidx", derive(Pack))]
233pub struct LimitOrderType {
234    pub limit_price: Decimal,
235    pub post_only: bool,
236}
237
238#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
239#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
240#[cfg_attr(feature = "netidx", derive(Pack))]
241pub struct StopLossLimitOrderType {
242    pub limit_price: Decimal,
243    pub trigger_price: Decimal,
244}
245
246#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
247#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
248#[cfg_attr(feature = "netidx", derive(Pack))]
249pub struct TakeProfitLimitOrderType {
250    pub limit_price: Decimal,
251    pub trigger_price: Decimal,
252}
253
254#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
255#[cfg_attr(feature = "netidx", derive(Pack))]
256#[serde(tag = "type", content = "value")]
257pub enum TimeInForce {
258    GoodTilCancel,
259    GoodTilDate(DateTime<Utc>),
260    /// Day order--the specific time which this expires will be dependent on the venue
261    GoodTilDay,
262    ImmediateOrCancel,
263    FillOrKill,
264}
265
266impl TimeInForce {
267    pub fn from_instruction(
268        instruction: &str,
269        good_til_date: Option<DateTime<Utc>>,
270    ) -> Result<Self> {
271        match instruction {
272            "GTC" => Ok(Self::GoodTilCancel),
273            "GTD" => Ok(Self::GoodTilDate(
274                good_til_date.ok_or_else(|| anyhow!("GTD requires good_til_date"))?,
275            )),
276            "DAY" => Ok(Self::GoodTilDay),
277            "IOC" => Ok(Self::ImmediateOrCancel),
278            "FOK" => Ok(Self::FillOrKill),
279            _ => Err(anyhow!("unknown time-in-force instruction: {}", instruction)),
280        }
281    }
282}
283
284#[cfg_attr(feature = "juniper", juniper::graphql_object)]
285impl TimeInForce {
286    pub fn instruction(&self) -> &'static str {
287        match self {
288            Self::GoodTilCancel => "GTC",
289            Self::GoodTilDate(_) => "GTD",
290            Self::GoodTilDay => "DAY",
291            Self::ImmediateOrCancel => "IOC",
292            Self::FillOrKill => "FOK",
293        }
294    }
295
296    pub fn good_til_date(&self) -> Option<DateTime<Utc>> {
297        match self {
298            Self::GoodTilDate(d) => Some(*d),
299            _ => None,
300        }
301    }
302}
303
304#[cfg(feature = "clap")]
305#[cfg_attr(feature = "clap", derive(clap::Args))]
306pub struct TimeInForceArgs {
307    /// GTC, GTD, IOC, DAY, FOK
308    #[arg(long, default_value = "GTC")]
309    time_in_force: String,
310    /// If TIF instruction is GTD, the datetime or relative duration from now;
311    /// e.g. +1d or 2021-01-01T00:00:00Z
312    #[arg(long)]
313    good_til_date: Option<String>,
314}
315
316#[cfg(feature = "clap")]
317impl TryInto<TimeInForce> for TimeInForceArgs {
318    type Error = anyhow::Error;
319
320    fn try_into(self) -> Result<TimeInForce> {
321        let good_til_date = self
322            .good_til_date
323            .map(|s| {
324                if s.starts_with('+') {
325                    let dur_s = &s[1..];
326                    let dur = crate::utils::duration::parse_duration(&dur_s)?;
327                    let now = Utc::now();
328                    Ok::<_, anyhow::Error>(now + dur)
329                } else {
330                    let dt = DateTime::parse_from_rfc3339(&s)?;
331                    Ok::<_, anyhow::Error>(dt.with_timezone(&Utc))
332                }
333            })
334            .transpose()?;
335        TimeInForce::from_instruction(&self.time_in_force, good_til_date)
336    }
337}
338
339/// The state of an order
340#[bitflags]
341#[repr(u8)]
342#[derive(
343    Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, JsonSchema_repr,
344)]
345#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
346#[cfg_attr(feature = "netidx", derive(Pack))]
347pub enum OrderStateFlags {
348    Open,
349    Rejected,
350    Acked,
351    Filled,
352    Canceling,
353    Canceled,
354    Out,
355    Stale, // we were expecting some state change but it was never confirmed
356}
357
358pub type OrderState = BitFlags<OrderStateFlags>;
359
360#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
361#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
362#[cfg_attr(feature = "netidx", derive(Pack))]
363pub struct Cancel {
364    pub order_id: OrderId,
365}
366
367#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, JsonSchema)]
368#[cfg_attr(feature = "netidx", derive(Pack))]
369pub struct CancelAll {
370    pub venue_id: Option<VenueId>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
374#[cfg_attr(feature = "netidx", derive(Pack))]
375pub struct Reject {
376    pub order_id: OrderId,
377    pub reason: RejectReason,
378}
379
380impl Reject {
381    pub fn new(order_id: OrderId, reason: RejectReason) -> Self {
382        Self { order_id, reason }
383    }
384
385    pub fn order_id(&self) -> OrderId {
386        self.order_id
387    }
388
389    pub fn reason(&self) -> String {
390        self.reason.to_string()
391    }
392}
393
394/// Reject reason, includes common reasons as unit enum variants,
395/// but leaves room for custom reasons if needed; although, performance
396/// sensitive components should still supertype their own rejects.
397#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
398#[cfg_attr(feature = "netidx", derive(Pack))]
399pub enum RejectReason {
400    // custom message...can be slow b/c sending the whole string
401    Literal(ArcStr),
402    ComponentNotInitialized,
403    UnknownCpty,
404    UnknownMarket,
405    DuplicateOrderId,
406    InvalidQuantity,
407    MissingRequiredAccount,
408    NoAccount,
409    NotAuthorized,
410    NotAuthorizedForAccount,
411    #[cfg_attr(feature = "netidx", pack(other))]
412    #[serde(other)]
413    Unknown,
414}
415
416impl std::fmt::Display for RejectReason {
417    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418        use RejectReason::*;
419        match self {
420            Literal(s) => write!(f, "{}", s),
421            ComponentNotInitialized => write!(f, "component not initialized"),
422            UnknownCpty => write!(f, "unknown cpty"),
423            UnknownMarket => write!(f, "unknown market"),
424            DuplicateOrderId => write!(f, "duplicate order id"),
425            InvalidQuantity => write!(f, "invalid quantity"),
426            MissingRequiredAccount => write!(f, "missing required account"),
427            NoAccount => write!(f, "no account"),
428            NotAuthorized => write!(f, "not authorized to perform action"),
429            NotAuthorizedForAccount => write!(f, "not authorized for account"),
430            Unknown => write!(f, "unknown"),
431        }
432    }
433}
434
435#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
436#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
437#[cfg_attr(feature = "netidx", derive(Pack))]
438pub struct Ack {
439    pub order_id: OrderId,
440}
441
442impl Ack {
443    pub fn new(order_id: OrderId) -> Self {
444        Self { order_id }
445    }
446}
447
448#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
449#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
450#[cfg_attr(feature = "netidx", derive(Pack))]
451pub struct Out {
452    pub order_id: OrderId,
453}
454
455impl Out {
456    pub fn new(order_id: OrderId) -> Self {
457        Self { order_id }
458    }
459}