1use std::collections::HashMap;
4use std::time::Instant;
5
6use brokapi_core::{BrokerOrderId, ClientOrderId, OrderState, Qty};
7
8#[derive(Debug, Clone, PartialEq)]
12pub enum OrderSignal {
13 PlaceSent,
15 Ack {
17 broker_id: BrokerOrderId,
19 },
20 PartialFill {
22 filled: Qty,
24 left: Qty,
26 },
27 Filled,
29 Cancelled,
31 Rejected {
33 reason: String,
35 },
36 Timeout,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum FsmError {
43 UnknownOrder(ClientOrderId),
45 InvalidTransition {
47 from: &'static str,
49 signal: &'static str,
51 },
52}
53
54#[derive(Debug, Clone)]
109pub struct OrderFsm {
110 state: OrderState,
111}
112
113impl OrderFsm {
114 pub fn new(cid: ClientOrderId, now: Instant) -> Self {
116 Self {
117 state: OrderState::InFlight { cid, sent_at: now },
118 }
119 }
120
121 pub fn from_state(state: OrderState) -> Self {
123 Self { state }
124 }
125
126 pub fn state(&self) -> &OrderState {
128 &self.state
129 }
130
131 pub fn cid(&self) -> ClientOrderId {
133 self.state.cid()
134 }
135
136 pub fn apply(
148 &mut self,
149 signal: OrderSignal,
150 now: Instant,
151 ) -> Result<Option<OrderState>, FsmError> {
152 let prev = self.state.clone();
153 let next = transition(&self.state, signal, now)?;
154 if next != prev {
155 self.state = next.clone();
156 Ok(Some(next))
157 } else {
158 Ok(None)
159 }
160 }
161}
162
163fn transition(
164 state: &OrderState,
165 signal: OrderSignal,
166 now: Instant,
167) -> Result<OrderState, FsmError> {
168 let cid = state.cid();
169
170 let signal_label = signal_name(&signal);
171
172 match (state, signal) {
173 (OrderState::InFlight { .. }, OrderSignal::PlaceSent) => Ok(state.clone()),
174
175 (
179 OrderState::InFlight { .. } | OrderState::Unknown { .. },
180 OrderSignal::Ack { broker_id },
181 ) => Ok(OrderState::Acked { cid, broker_id }),
182 (
183 OrderState::InFlight { .. } | OrderState::Unknown { .. } | OrderState::Acked { .. },
184 OrderSignal::PartialFill { filled, left },
185 ) => Ok(OrderState::PartiallyFilled { cid, filled, left }),
186 (
187 OrderState::InFlight { .. }
188 | OrderState::Unknown { .. }
189 | OrderState::Acked { .. }
190 | OrderState::PartiallyFilled { .. },
191 OrderSignal::Filled,
192 ) => Ok(OrderState::Filled { cid }),
193 (
194 OrderState::InFlight { .. }
195 | OrderState::Unknown { .. }
196 | OrderState::Acked { .. }
197 | OrderState::PartiallyFilled { .. },
198 OrderSignal::Cancelled,
199 ) => Ok(OrderState::Cancelled { cid }),
200 (
201 OrderState::InFlight { .. }
202 | OrderState::Unknown { .. }
203 | OrderState::Acked { .. }
204 | OrderState::PartiallyFilled { .. },
205 OrderSignal::Rejected { reason },
206 ) => Ok(OrderState::Rejected { cid, reason }),
207
208 (OrderState::InFlight { .. }, OrderSignal::Timeout) => {
209 Ok(OrderState::Unknown { cid, since: now })
210 }
211
212 (
213 OrderState::PartiallyFilled { filled: have, .. },
214 OrderSignal::PartialFill { filled, left },
215 ) => {
216 if filled < *have {
218 Ok(state.clone())
219 } else {
220 Ok(OrderState::PartiallyFilled { cid, filled, left })
221 }
222 }
223
224 (
228 OrderState::Unknown { .. }
229 | OrderState::Acked { .. }
230 | OrderState::PartiallyFilled { .. }
231 | OrderState::Filled { .. }
232 | OrderState::Cancelled { .. }
233 | OrderState::Rejected { .. },
234 OrderSignal::Timeout | OrderSignal::PlaceSent | OrderSignal::Ack { .. },
235 )
236 | (
237 OrderState::Filled { .. } | OrderState::Cancelled { .. } | OrderState::Rejected { .. },
238 OrderSignal::PartialFill { .. },
239 )
240 | (OrderState::Filled { .. }, OrderSignal::Filled)
241 | (OrderState::Cancelled { .. }, OrderSignal::Cancelled)
242 | (OrderState::Rejected { .. }, OrderSignal::Rejected { .. }) => Ok(state.clone()),
243
244 (from, _) => Err(FsmError::InvalidTransition {
246 from: state_name(from),
247 signal: signal_label,
248 }),
249 }
250}
251
252fn state_name(state: &OrderState) -> &'static str {
253 state.name()
254}
255
256fn signal_name(signal: &OrderSignal) -> &'static str {
257 match signal {
258 OrderSignal::PlaceSent => "PlaceSent",
259 OrderSignal::Ack { .. } => "Ack",
260 OrderSignal::PartialFill { .. } => "PartialFill",
261 OrderSignal::Filled => "Filled",
262 OrderSignal::Cancelled => "Cancelled",
263 OrderSignal::Rejected { .. } => "Rejected",
264 OrderSignal::Timeout => "Timeout",
265 }
266}
267
268#[derive(Debug, Default)]
291pub struct OrderRegistry {
292 orders: HashMap<ClientOrderId, OrderFsm>,
293}
294
295impl OrderRegistry {
296 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn register_place(&mut self, cid: ClientOrderId, now: Instant) {
304 self.orders.insert(cid, OrderFsm::new(cid, now));
305 }
306
307 pub fn apply(
313 &mut self,
314 cid: ClientOrderId,
315 signal: OrderSignal,
316 now: Instant,
317 ) -> Result<Option<OrderState>, FsmError> {
318 let fsm = self
319 .orders
320 .get_mut(&cid)
321 .ok_or(FsmError::UnknownOrder(cid))?;
322 fsm.apply(signal, now)
323 }
324
325 pub fn get(&self, cid: ClientOrderId) -> Option<&OrderFsm> {
327 self.orders.get(&cid)
328 }
329
330 pub fn forget_finished(&mut self, cid: ClientOrderId) -> bool {
333 let finished = self.orders.get(&cid).is_some_and(|f| f.state().is_final());
334 if finished {
335 self.orders.remove(&cid);
336 }
337 finished
338 }
339
340 pub fn len(&self) -> usize {
342 self.orders.len()
343 }
344
345 pub fn is_empty(&self) -> bool {
347 self.orders.is_empty()
348 }
349
350 pub fn in_flight_cids(&self) -> Vec<ClientOrderId> {
352 self.orders
353 .iter()
354 .filter_map(|(cid, fsm)| {
355 if matches!(fsm.state(), OrderState::InFlight { .. }) {
356 Some(*cid)
357 } else {
358 None
359 }
360 })
361 .collect()
362 }
363
364 pub fn abandon_inflight(&mut self, now: Instant) -> Vec<(ClientOrderId, OrderState)> {
368 let inflight: Vec<ClientOrderId> = self.in_flight_cids();
369 let mut abandoned = Vec::new();
370 for cid in inflight {
371 if let Ok(Some(next)) = self.apply(cid, OrderSignal::Timeout, now) {
372 abandoned.push((cid, next));
373 }
374 }
375 abandoned
376 }
377
378 pub fn on_global_timeout(
383 &mut self,
384 now: Instant,
385 timeout: std::time::Duration,
386 ) -> Vec<(ClientOrderId, OrderState)> {
387 let mut changed = Vec::new();
388 for (cid, fsm) in &mut self.orders {
389 if let OrderState::InFlight { sent_at, .. } = fsm.state()
390 && now.duration_since(*sent_at) >= timeout
391 && let Ok(Some(next)) = fsm.apply(OrderSignal::Timeout, now)
392 {
393 changed.push((*cid, next));
394 }
395 }
396 changed
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use std::time::Duration;
404
405 fn cid(n: u8) -> ClientOrderId {
406 ClientOrderId::from_bytes([n; 16])
407 }
408
409 fn now() -> Instant {
410 Instant::now()
411 }
412
413 #[test]
414 fn in_flight_ack_to_acked() {
415 let mut fsm = OrderFsm::new(cid(1), now());
416 let next = fsm
417 .apply(
418 OrderSignal::Ack {
419 broker_id: BrokerOrderId::new("42"),
420 },
421 now(),
422 )
423 .unwrap()
424 .unwrap();
425 assert!(matches!(next, OrderState::Acked { .. }));
426 }
427
428 #[test]
429 fn in_flight_timeout_to_unknown() {
430 let mut fsm = OrderFsm::new(cid(1), now());
431 let next = fsm.apply(OrderSignal::Timeout, now()).unwrap().unwrap();
432 assert!(matches!(next, OrderState::Unknown { .. }));
433 }
434
435 #[test]
436 fn unknown_recovery_via_ack() {
437 let mut fsm = OrderFsm::from_state(OrderState::Unknown {
438 cid: cid(1),
439 since: now(),
440 });
441 let next = fsm
442 .apply(
443 OrderSignal::Ack {
444 broker_id: BrokerOrderId::new("99"),
445 },
446 now(),
447 )
448 .unwrap()
449 .unwrap();
450 assert!(matches!(next, OrderState::Acked { .. }));
451 }
452
453 #[test]
454 fn terminal_states_reject_signals() {
455 let mut fsm = OrderFsm::from_state(OrderState::Filled { cid: cid(1) });
456 assert!(fsm.apply(OrderSignal::Cancelled, now()).is_err());
457 }
458
459 #[test]
460 fn registry_timeout_batch() {
461 let mut reg = OrderRegistry::new();
462 let t0 = Instant::now();
463 reg.register_place(cid(1), t0);
464 let changed = reg.on_global_timeout(t0 + Duration::from_secs(31), Duration::from_secs(30));
465 assert_eq!(changed.len(), 1);
466 assert!(matches!(changed[0].1, OrderState::Unknown { .. }));
467 }
468
469 fn ack(id: &str) -> OrderSignal {
470 OrderSignal::Ack {
471 broker_id: BrokerOrderId::new(id),
472 }
473 }
474
475 fn partial(filled: i64, left: i64) -> OrderSignal {
476 OrderSignal::PartialFill {
477 filled: Qty::from(filled),
478 left: Qty::from(left),
479 }
480 }
481
482 #[test]
483 fn news_that_is_faster_than_the_answer_is_taken() {
484 let mut fsm = OrderFsm::new(cid(1), now());
486 assert!(matches!(
487 fsm.apply(OrderSignal::Filled, now()),
488 Ok(Some(OrderState::Filled { .. }))
489 ));
490 let mut fsm = OrderFsm::new(cid(2), now());
491 assert!(matches!(
492 fsm.apply(partial(1, 2), now()),
493 Ok(Some(OrderState::PartiallyFilled { .. }))
494 ));
495 let mut fsm = OrderFsm::new(cid(3), now());
496 assert!(matches!(
497 fsm.apply(OrderSignal::Cancelled, now()),
498 Ok(Some(OrderState::Cancelled { .. }))
499 ));
500 let mut fsm = OrderFsm::from_state(OrderState::Unknown {
502 cid: cid(4),
503 since: now(),
504 });
505 assert!(matches!(
506 fsm.apply(partial(1, 2), now()),
507 Ok(Some(OrderState::PartiallyFilled { .. }))
508 ));
509 }
510
511 #[test]
512 fn a_late_answer_changes_nothing() {
513 let mut fsm = OrderFsm::new(cid(1), now());
515 fsm.apply(OrderSignal::Filled, now()).unwrap();
516 for late in [
517 ack("42"),
518 partial(1, 1),
519 OrderSignal::Timeout,
520 OrderSignal::PlaceSent,
521 ] {
522 assert_eq!(fsm.apply(late, now()), Ok(None));
523 }
524 assert!(matches!(fsm.state(), OrderState::Filled { .. }));
525 assert_eq!(fsm.apply(OrderSignal::Filled, now()), Ok(None));
527 let mut cancelled = OrderFsm::from_state(OrderState::Cancelled { cid: cid(2) });
528 assert_eq!(cancelled.apply(OrderSignal::Cancelled, now()), Ok(None));
529 let mut rejected = OrderFsm::from_state(OrderState::Rejected {
530 cid: cid(3),
531 reason: "no".into(),
532 });
533 assert_eq!(
534 rejected.apply(
535 OrderSignal::Rejected {
536 reason: "no".into()
537 },
538 now()
539 ),
540 Ok(None)
541 );
542 let mut working = OrderFsm::from_state(OrderState::Acked {
544 cid: cid(4),
545 broker_id: BrokerOrderId::new("1"),
546 });
547 assert_eq!(working.apply(ack("1"), now()), Ok(None));
548 let mut part = OrderFsm::from_state(OrderState::PartiallyFilled {
549 cid: cid(5),
550 filled: Qty::from(1),
551 left: Qty::from(1),
552 });
553 assert_eq!(part.apply(ack("1"), now()), Ok(None));
554 }
555
556 #[test]
557 fn an_older_snapshot_does_not_take_a_fill_back() {
558 let mut fsm = OrderFsm::from_state(OrderState::PartiallyFilled {
559 cid: cid(1),
560 filled: Qty::from(5),
561 left: Qty::from(5),
562 });
563 assert_eq!(fsm.apply(partial(3, 7), now()), Ok(None));
565 assert!(matches!(
566 fsm.state(),
567 OrderState::PartiallyFilled { filled, .. } if *filled == Qty::from(5)
568 ));
569 assert!(matches!(fsm.apply(partial(6, 4), now()), Ok(Some(_))));
571 }
572
573 #[test]
574 fn contradicting_news_is_an_error_and_leaves_the_state() {
575 let mut fsm = OrderFsm::from_state(OrderState::Filled { cid: cid(1) });
576 assert_eq!(
577 fsm.apply(OrderSignal::Cancelled, now()),
578 Err(FsmError::InvalidTransition {
579 from: "Filled",
580 signal: "Cancelled"
581 })
582 );
583 assert!(matches!(fsm.state(), OrderState::Filled { .. }));
584 let mut cancelled = OrderFsm::from_state(OrderState::Cancelled { cid: cid(2) });
585 assert!(cancelled.apply(OrderSignal::Filled, now()).is_err());
586 }
587
588 #[test]
589 fn the_registry_forgets_only_finished_orders() {
590 let mut reg = OrderRegistry::new();
591 let t0 = Instant::now();
592 reg.register_place(cid(1), t0);
593 reg.register_place(cid(2), t0);
594 reg.apply(cid(2), OrderSignal::Filled, t0).unwrap();
595 assert_eq!(reg.len(), 2);
596 assert!(!reg.is_empty());
597 assert!(!reg.forget_finished(cid(1)), "a live order stays");
598 assert!(!reg.forget_finished(cid(9)), "an unknown cid is left alone");
599 assert!(reg.forget_finished(cid(2)));
600 assert_eq!(reg.len(), 1);
601 assert!(reg.get(cid(2)).is_none());
602 }
603}