pub struct OrderFsm { /* private fields */ }Expand description
The life of one order as a finite state machine over [OrderState].
The order starts InFlight (the place command is on the wire). A broker answer moves it on. If
no answer comes, OrderSignal::Timeout moves it to Unknown. Unknown is not a failure: the
order may exist at the broker, and a later ack, fill or cancel (from the stream or from a
reconcile) settles it. The command is never repeated, because a retry could create a second
order.
The machine takes the order in which news really arrives. The stream of fills can be faster than the
answer to the place command, so a fill or a cancel is valid in InFlight too.
| State | Signal -> new state |
|---|---|
InFlight | PlaceSent -> no change, Ack -> Acked, PartialFill -> PartiallyFilled, Filled, Cancelled, Rejected, Timeout -> Unknown |
Unknown | Ack -> Acked, PartialFill -> PartiallyFilled, Filled, Cancelled, Rejected, Timeout -> no change |
Acked | PartialFill -> PartiallyFilled, Filled, Cancelled, Rejected, Ack -> no change (late repeat) |
PartiallyFilled | PartialFill -> PartiallyFilled (new quantities, but a smaller filled than known is a stale snapshot and changes nothing), Filled, Cancelled, Rejected, Ack -> no change |
Filled, Cancelled, Rejected | the same final state again, and a late Ack or PartialFill -> no change. A different final state (a cancel of a filled order) -> FsmError::InvalidTransition |
News that is late or repeated but agrees with what is known changes nothing (Ok(None)). News that
contradicts it is an error and the state is left as it was (the order book of brokapi-runtime logs
it at warn level).
§Examples
use std::time::Instant;
use brokapi_core::{BrokerOrderId, ClientOrderId, OrderState};
use brokapi_sansio::{FsmError, OrderFsm, OrderSignal};
let cid = ClientOrderId::new_v4();
let now = Instant::now();
let mut fsm = OrderFsm::new(cid, now);
assert!(matches!(fsm.state(), OrderState::InFlight { .. }));
// No answer in time: the outcome is unknown.
let next = fsm.apply(OrderSignal::Timeout, now)?;
assert!(matches!(next, Some(OrderState::Unknown { .. })));
// A second timeout changes nothing: Ok(None).
assert_eq!(fsm.apply(OrderSignal::Timeout, now), Ok(None));
// The stream later shows the order. It was alive all along.
let next = fsm.apply(OrderSignal::Ack { broker_id: BrokerOrderId::new("42") }, now)?;
assert!(matches!(next, Some(OrderState::Acked { .. })));
fsm.apply(OrderSignal::Filled, now)?;
// A finished order accepts nothing that contradicts it.
assert!(matches!(
fsm.apply(OrderSignal::Cancelled, now),
Err(FsmError::InvalidTransition { from: "Filled", signal: "Cancelled" })
));Implementations§
Source§impl OrderFsm
impl OrderFsm
Sourcepub fn new(cid: ClientOrderId, now: Instant) -> Self
pub fn new(cid: ClientOrderId, now: Instant) -> Self
Starts tracking an order whose place command is sent at now, in state InFlight.
Sourcepub fn from_state(state: OrderState) -> Self
pub fn from_state(state: OrderState) -> Self
Starts from a known state, for example an order read back from the broker.
Sourcepub fn apply(
&mut self,
signal: OrderSignal,
now: Instant,
) -> Result<Option<OrderState>, FsmError>
pub fn apply( &mut self, signal: OrderSignal, now: Instant, ) -> Result<Option<OrderState>, FsmError>
Applies a signal at now.
Returns Ok(Some(new_state)) when the state changed, and Ok(None) when the signal is
valid but changes nothing (a repeated Timeout in Unknown, PlaceSent in InFlight, a
PartialFill with the same quantities). now is stored in the Unknown state as the time
the doubt began.
§Errors
FsmError::InvalidTransition when the signal is not valid in the current state, see the
table in the type docs. The state is left unchanged.