1use crate::order::{
2 id::StrategyId,
3 request::{OrderRequestCancel, OrderRequestOpen, RequestCancel, RequestOpen},
4 state::UnindexedOrderState,
5};
6use chrono::{DateTime, Utc};
7use derive_more::{Constructor, Display};
8use id::ClientOrderId;
9use rust_decimal::Decimal;
10use rustrade_instrument::{
11 Side,
12 asset::{AssetIndex, name::AssetNameExchange},
13 exchange::{ExchangeId, ExchangeIndex},
14 instrument::{InstrumentIndex, name::InstrumentNameExchange},
15};
16use serde::{Deserialize, Serialize};
17use state::{
18 ActiveOrderState, Cancelled, Expired, InactiveOrderState, Open, OpenInFlight, OrderState,
19};
20
21pub mod id;
23
24pub mod state;
28
29pub mod request;
33
34pub mod bracket;
36
37pub type UnindexedOrder = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>;
39
40pub type UnindexedOrderKey = OrderKey<ExchangeId, InstrumentNameExchange>;
43
44pub type UnindexedInactiveOrder =
47 Order<ExchangeId, InstrumentNameExchange, state::UnindexedInactiveOrderState>;
48
49pub type UnindexedOrderSnapshot = Order<
52 ExchangeId,
53 InstrumentNameExchange,
54 OrderState<AssetNameExchange, InstrumentNameExchange>,
55>;
56
57pub type OrderSnapshot<
59 ExchangeKey = ExchangeIndex,
60 AssetKey = AssetIndex,
61 InstrumentKey = InstrumentIndex,
62> = Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>;
63
64#[derive(
65 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
66)]
67
68pub struct OrderEvent<State, ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
69 pub key: OrderKey<ExchangeKey, InstrumentKey>,
70 pub state: State,
71}
72
73#[derive(
74 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
75)]
76pub struct OrderKey<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex> {
77 pub exchange: ExchangeKey,
78 pub instrument: InstrumentKey,
79 pub strategy: StrategyId,
80 pub cid: ClientOrderId,
81}
82
83impl<ExchangeKey, InstrumentKey> OrderKey<ExchangeKey, &InstrumentKey>
84where
85 InstrumentKey: Clone,
86{
87 pub fn into_owned_instrument(self) -> OrderKey<ExchangeKey, InstrumentKey> {
94 let Self {
95 exchange,
96 instrument,
97 strategy,
98 cid,
99 } = self;
100
101 OrderKey {
102 exchange,
103 instrument: instrument.clone(),
104 strategy,
105 cid,
106 }
107 }
108}
109
110impl<State, ExchangeKey, InstrumentKey> OrderEvent<State, ExchangeKey, &InstrumentKey>
111where
112 InstrumentKey: Clone,
113{
114 pub fn into_owned_instrument(self) -> OrderEvent<State, ExchangeKey, InstrumentKey> {
118 let Self { key, state } = self;
119
120 OrderEvent {
121 key: key.into_owned_instrument(),
122 state,
123 }
124 }
125}
126
127#[derive(
128 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Constructor,
129)]
130pub struct Order<ExchangeKey = ExchangeIndex, InstrumentKey = InstrumentIndex, State = OrderState> {
131 pub key: OrderKey<ExchangeKey, InstrumentKey>,
132 pub side: Side,
133 pub price: Option<Decimal>,
137 pub quantity: Decimal,
138 pub kind: OrderKind,
139 pub time_in_force: TimeInForce,
140 pub state: State,
141}
142
143impl<ExchangeKey, InstrumentKey, State> Order<ExchangeKey, InstrumentKey, State> {
144 pub fn map_state<NewState>(
146 self,
147 f: impl FnOnce(State) -> NewState,
148 ) -> Order<ExchangeKey, InstrumentKey, NewState> {
149 let Order {
150 key,
151 side,
152 price,
153 quantity,
154 kind,
155 time_in_force,
156 state,
157 } = self;
158
159 Order {
160 key,
161 side,
162 price,
163 quantity,
164 kind,
165 time_in_force,
166 state: f(state),
167 }
168 }
169}
170
171impl<ExchangeKey, AssetKey, InstrumentKey>
172 Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
173{
174 pub fn to_active(&self) -> Option<Order<ExchangeKey, InstrumentKey, ActiveOrderState>>
175 where
176 ExchangeKey: Clone,
177 InstrumentKey: Clone,
178 {
179 let OrderState::Active(state) = &self.state else {
180 return None;
181 };
182
183 Some(Order {
184 key: self.key.clone(),
185 side: self.side,
186 price: self.price,
187 quantity: self.quantity,
188 kind: self.kind,
189 time_in_force: self.time_in_force,
190 state: state.clone(),
191 })
192 }
193
194 pub fn to_inactive(
195 &self,
196 ) -> Option<Order<ExchangeKey, InstrumentKey, InactiveOrderState<AssetKey, InstrumentKey>>>
197 where
198 ExchangeKey: Clone,
199 AssetKey: Clone,
200 InstrumentKey: Clone,
201 {
202 let OrderState::Inactive(state) = &self.state else {
203 return None;
204 };
205
206 Some(Order {
207 key: self.key.clone(),
208 side: self.side,
209 price: self.price,
210 quantity: self.quantity,
211 kind: self.kind,
212 time_in_force: self.time_in_force,
213 state: state.clone(),
214 })
215 }
216}
217
218impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
219where
220 ExchangeKey: Clone,
221 InstrumentKey: Clone,
222{
223 pub fn to_request_cancel(&self) -> Option<OrderRequestCancel<ExchangeKey, InstrumentKey>> {
224 let Order { key, state, .. } = self;
225
226 let request_cancel = match state {
227 ActiveOrderState::OpenInFlight(_) => RequestCancel { id: None },
228 ActiveOrderState::Open(open) => RequestCancel {
229 id: Some(open.id.clone()),
230 },
231 _ => return None,
232 };
233
234 Some(OrderRequestCancel {
235 key: key.clone(),
236 state: request_cancel,
237 })
238 }
239}
240
241#[derive(
248 Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
249)]
250pub enum TrailingOffsetType {
251 Absolute,
253 Percentage,
255 BasisPoints,
257}
258
259#[derive(
260 Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
261)]
262#[non_exhaustive]
263pub enum OrderKind {
264 Market,
265 Limit,
266 #[display("Stop({trigger_price})")]
268 Stop {
269 trigger_price: Decimal,
270 },
271 #[display("StopLimit({trigger_price})")]
273 StopLimit {
274 trigger_price: Decimal,
275 },
276 #[display("TakeProfit({trigger_price})")]
279 TakeProfit {
280 trigger_price: Decimal,
281 },
282 #[display("TakeProfitLimit({trigger_price})")]
284 TakeProfitLimit {
285 trigger_price: Decimal,
286 },
287 #[display("TrailingStop({offset}, {offset_type}, activation: {activation_price:?})")]
289 TrailingStop {
290 offset: Decimal,
291 offset_type: TrailingOffsetType,
292 activation_price: Option<Decimal>,
305 },
306 #[display("TrailingStopLimit({offset}, {offset_type}, {stop_price}, {limit_offset})")]
310 TrailingStopLimit {
311 offset: Decimal,
312 offset_type: TrailingOffsetType,
313 stop_price: Decimal,
321 limit_offset: Decimal,
323 },
324 #[display("TrailingStopFixedLimit({offset}, {offset_type}, activation: {activation_price:?})")]
333 TrailingStopFixedLimit {
334 offset: Decimal,
335 offset_type: TrailingOffsetType,
336 activation_price: Option<Decimal>,
338 },
339 #[display("ConditionalUnresolved({family}, trigger: {trigger_price:?})")]
354 ConditionalUnresolved {
355 family: ConditionalFamily,
356 trigger_price: Option<Decimal>,
360 },
361}
362
363impl OrderKind {
364 pub(crate) fn is_conditional_unresolved(&self) -> bool {
367 matches!(self, Self::ConditionalUnresolved { .. })
368 }
369}
370
371#[derive(
376 Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
377)]
378#[non_exhaustive]
379pub enum ConditionalFamily {
380 Stop,
382 StopLimit,
385 TakeProfit,
388 TakeProfitLimit,
391}
392
393impl ConditionalFamily {
394 pub fn has_limit_price(self) -> bool {
396 matches!(self, Self::StopLimit | Self::TakeProfitLimit)
397 }
398}
399
400#[derive(
401 Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
402)]
403pub enum TimeInForce {
404 GoodUntilCancelled {
405 post_only: bool,
406 },
407 GoodUntilEndOfDay,
408 FillOrKill,
409 ImmediateOrCancel,
410 #[display("GoodTillDate({expiry})")]
412 GoodTillDate {
413 expiry: DateTime<Utc>,
414 },
415 AtOpen,
417 AtClose,
419}
420
421impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
422where
423 ExchangeKey: Clone,
424 InstrumentKey: Clone,
425{
426 pub fn open_in_flight(
428 request: &OrderRequestOpen<ExchangeKey, InstrumentKey>,
429 time_sent: DateTime<Utc>,
430 ) -> Self {
431 let OrderRequestOpen {
432 key,
433 state:
434 RequestOpen {
435 side,
436 price,
437 quantity,
438 kind,
439 time_in_force,
440 position_id: _,
441 reduce_only: _, market: _, margin_effect: _, },
445 } = request;
446
447 Self {
448 key: key.clone(),
449 side: *side,
450 price: *price,
451 quantity: *quantity,
452 kind: *kind,
453 time_in_force: *time_in_force,
454 state: ActiveOrderState::OpenInFlight(OpenInFlight::new(time_sent)),
455 }
456 }
457}
458
459impl<ExchangeKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
460 for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
461{
462 fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
463 value.map_state(ActiveOrderState::Open)
464 }
465}
466
467impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
468 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
469{
470 fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
471 value.map_state(|state| OrderState::Active(ActiveOrderState::Open(state)))
472 }
473}
474
475impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Cancelled>>
476 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
477{
478 fn from(value: Order<ExchangeKey, InstrumentKey, Cancelled>) -> Self {
479 value.map_state(|state| OrderState::Inactive(InactiveOrderState::Cancelled(state)))
480 }
481}
482
483impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Expired>>
484 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
485{
486 fn from(value: Order<ExchangeKey, InstrumentKey, Expired>) -> Self {
487 value.map_state(|state| OrderState::Inactive(InactiveOrderState::Expired(state)))
488 }
489}
490
491#[cfg(test)]
492#[allow(clippy::unwrap_used)] mod tests {
494 use super::*;
495 use rust_decimal_macros::dec;
496
497 #[test]
498 fn order_kind_display_take_profit() {
499 let tp = OrderKind::TakeProfit {
500 trigger_price: dec!(100.50),
501 };
502 assert_eq!(tp.to_string(), "TakeProfit(100.50)");
503 }
504
505 #[test]
506 fn order_kind_display_take_profit_limit() {
507 let tpl = OrderKind::TakeProfitLimit {
508 trigger_price: dec!(200.25),
509 };
510 assert_eq!(tpl.to_string(), "TakeProfitLimit(200.25)");
511 }
512
513 #[test]
514 fn order_kind_serde_roundtrip_take_profit() {
515 let original = OrderKind::TakeProfit {
516 trigger_price: dec!(150.00),
517 };
518 let json = serde_json::to_string(&original).unwrap();
519 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
520 assert_eq!(original, deserialized);
521 }
522
523 #[test]
524 fn order_kind_serde_roundtrip_take_profit_limit() {
525 let original = OrderKind::TakeProfitLimit {
526 trigger_price: dec!(250.75),
527 };
528 let json = serde_json::to_string(&original).unwrap();
529 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
530 assert_eq!(original, deserialized);
531 }
532 #[test]
535 fn trailing_kinds_round_trip_and_a_stored_trailing_stop_loads() {
536 for activation_price in [None, Some(dec!(105))] {
537 for original in [
538 OrderKind::TrailingStop {
539 offset: dec!(1),
540 offset_type: TrailingOffsetType::Percentage,
541 activation_price,
542 },
543 OrderKind::TrailingStopFixedLimit {
544 offset: dec!(25),
545 offset_type: TrailingOffsetType::BasisPoints,
546 activation_price,
547 },
548 ] {
549 let json = serde_json::to_string(&original).unwrap();
550 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
551 assert_eq!(original, deserialized);
552 }
553 }
554 let stored: OrderKind =
555 serde_json::from_str(r#"{"TrailingStop":{"offset":"1","offset_type":"Percentage"}}"#)
556 .unwrap();
557 assert_eq!(
558 stored,
559 OrderKind::TrailingStop {
560 offset: dec!(1),
561 offset_type: TrailingOffsetType::Percentage,
562 activation_price: None,
563 }
564 );
565 assert_eq!(
566 OrderKind::TrailingStop {
567 offset: dec!(1),
568 offset_type: TrailingOffsetType::Percentage,
569 activation_price: Some(dec!(105)),
570 }
571 .to_string(),
572 "TrailingStop(1, Percentage, activation: Some(105))"
573 );
574 }
575}