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}
340
341#[derive(
342 Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, Display,
343)]
344pub enum TimeInForce {
345 GoodUntilCancelled {
346 post_only: bool,
347 },
348 GoodUntilEndOfDay,
349 FillOrKill,
350 ImmediateOrCancel,
351 #[display("GoodTillDate({expiry})")]
353 GoodTillDate {
354 expiry: DateTime<Utc>,
355 },
356 AtOpen,
358 AtClose,
360}
361
362impl<ExchangeKey, InstrumentKey> Order<ExchangeKey, InstrumentKey, ActiveOrderState>
363where
364 ExchangeKey: Clone,
365 InstrumentKey: Clone,
366{
367 pub fn open_in_flight(
369 request: &OrderRequestOpen<ExchangeKey, InstrumentKey>,
370 time_sent: DateTime<Utc>,
371 ) -> Self {
372 let OrderRequestOpen {
373 key,
374 state:
375 RequestOpen {
376 side,
377 price,
378 quantity,
379 kind,
380 time_in_force,
381 position_id: _,
382 reduce_only: _, market: _, margin_effect: _, },
386 } = request;
387
388 Self {
389 key: key.clone(),
390 side: *side,
391 price: *price,
392 quantity: *quantity,
393 kind: *kind,
394 time_in_force: *time_in_force,
395 state: ActiveOrderState::OpenInFlight(OpenInFlight::new(time_sent)),
396 }
397 }
398}
399
400impl<ExchangeKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
401 for Order<ExchangeKey, InstrumentKey, ActiveOrderState>
402{
403 fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
404 value.map_state(ActiveOrderState::Open)
405 }
406}
407
408impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Open>>
409 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
410{
411 fn from(value: Order<ExchangeKey, InstrumentKey, Open>) -> Self {
412 value.map_state(|state| OrderState::Active(ActiveOrderState::Open(state)))
413 }
414}
415
416impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Cancelled>>
417 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
418{
419 fn from(value: Order<ExchangeKey, InstrumentKey, Cancelled>) -> Self {
420 value.map_state(|state| OrderState::Inactive(InactiveOrderState::Cancelled(state)))
421 }
422}
423
424impl<ExchangeKey, AssetKey, InstrumentKey> From<Order<ExchangeKey, InstrumentKey, Expired>>
425 for Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>
426{
427 fn from(value: Order<ExchangeKey, InstrumentKey, Expired>) -> Self {
428 value.map_state(|state| OrderState::Inactive(InactiveOrderState::Expired(state)))
429 }
430}
431
432#[cfg(test)]
433#[allow(clippy::unwrap_used)] mod tests {
435 use super::*;
436 use rust_decimal_macros::dec;
437
438 #[test]
439 fn order_kind_display_take_profit() {
440 let tp = OrderKind::TakeProfit {
441 trigger_price: dec!(100.50),
442 };
443 assert_eq!(tp.to_string(), "TakeProfit(100.50)");
444 }
445
446 #[test]
447 fn order_kind_display_take_profit_limit() {
448 let tpl = OrderKind::TakeProfitLimit {
449 trigger_price: dec!(200.25),
450 };
451 assert_eq!(tpl.to_string(), "TakeProfitLimit(200.25)");
452 }
453
454 #[test]
455 fn order_kind_serde_roundtrip_take_profit() {
456 let original = OrderKind::TakeProfit {
457 trigger_price: dec!(150.00),
458 };
459 let json = serde_json::to_string(&original).unwrap();
460 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
461 assert_eq!(original, deserialized);
462 }
463
464 #[test]
465 fn order_kind_serde_roundtrip_take_profit_limit() {
466 let original = OrderKind::TakeProfitLimit {
467 trigger_price: dec!(250.75),
468 };
469 let json = serde_json::to_string(&original).unwrap();
470 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
471 assert_eq!(original, deserialized);
472 }
473 #[test]
476 fn trailing_kinds_round_trip_and_a_stored_trailing_stop_loads() {
477 for activation_price in [None, Some(dec!(105))] {
478 for original in [
479 OrderKind::TrailingStop {
480 offset: dec!(1),
481 offset_type: TrailingOffsetType::Percentage,
482 activation_price,
483 },
484 OrderKind::TrailingStopFixedLimit {
485 offset: dec!(25),
486 offset_type: TrailingOffsetType::BasisPoints,
487 activation_price,
488 },
489 ] {
490 let json = serde_json::to_string(&original).unwrap();
491 let deserialized: OrderKind = serde_json::from_str(&json).unwrap();
492 assert_eq!(original, deserialized);
493 }
494 }
495 let stored: OrderKind =
496 serde_json::from_str(r#"{"TrailingStop":{"offset":"1","offset_type":"Percentage"}}"#)
497 .unwrap();
498 assert_eq!(
499 stored,
500 OrderKind::TrailingStop {
501 offset: dec!(1),
502 offset_type: TrailingOffsetType::Percentage,
503 activation_price: None,
504 }
505 );
506 assert_eq!(
507 OrderKind::TrailingStop {
508 offset: dec!(1),
509 offset_type: TrailingOffsetType::Percentage,
510 activation_price: Some(dec!(105)),
511 }
512 .to_string(),
513 "TrailingStop(1, Percentage, activation: Some(105))"
514 );
515 }
516}