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(
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 }
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 pub fn with_trader(&mut self, trader: Option<UserId>) -> &mut Self {
135 self.trader = Some(trader);
136 self
137 }
138
139 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 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 #[arg(long, default_value = "GTC")]
309 time_in_force: String,
310 #[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#[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, }
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#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
398#[cfg_attr(feature = "netidx", derive(Pack))]
399pub enum RejectReason {
400 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}