pub struct OrderRuntime { /* private fields */ }Expand description
Runtime order book: one state machine per client order id, a timeout sweep, and a guard that never lets the same place command go out twice.
The rule: a place command that got no answer is never repeated. A retry could create a second
order at the broker. Instead the order goes to [OrderState::Unknown] and is settled by reading
the broker (reconcile or order state). try_place_once enforces the
“once”, and sweep_timeouts turns silent in-flight orders into
Unknown.
§Examples
use std::time::{Duration, Instant};
use brokapi_core::{BrokerOrderId, ClientOrderId, OrderState};
use brokapi_runtime::OrderRuntime;
use brokapi_sansio::OrderSignal;
let mut orders = OrderRuntime::with_timeout(Duration::from_secs(30));
let t0 = Instant::now();
let cid = ClientOrderId::new_v4();
// First send of this cid is allowed. A second send of the same cid is not.
assert!(orders.try_place_once(cid));
orders.register_place(cid, t0);
assert!(!orders.try_place_once(cid));
// No answer within the timeout: the outcome is unknown, not "failed".
let changed = orders.sweep_timeouts(t0 + Duration::from_secs(31));
assert!(matches!(changed[0].1, OrderState::Unknown { .. }));
// A late ack from the stream or from a reconcile settles it.
let acked = orders.apply_signal(cid, OrderSignal::Ack { broker_id: BrokerOrderId::new("42") }, t0);
assert!(matches!(acked, Some(OrderState::Acked { .. })));§Memory
A finished order (filled, cancelled, rejected) is kept for a window and then forgotten, so a bot that
trades for months does not grow without end. The window is RETENTION (24 hours) and at most
RETAINED_FINISHED finished orders, whichever ends first (see
with_retention). The guarantee “a cid is placed once” holds inside
the window. A cid older than the window is no longer remembered here, so a place with it would pass
the local check (the broker may still refuse it: T-Bank does, by its idempotency key). Never reuse a
cid: make a new one for every order.
Implementations§
Source§impl OrderRuntime
impl OrderRuntime
Sourcepub fn new() -> Self
pub fn new() -> Self
Creates an empty order book with a command timeout of 30 seconds and the default memory window
(RETENTION, RETAINED_FINISHED).
Sourcepub fn with_timeout(command_timeout: Duration) -> Self
pub fn with_timeout(command_timeout: Duration) -> Self
Creates an empty order book with the given command timeout, the age after which an in-flight
order is considered unanswered by sweep_timeouts.
Sourcepub fn set_command_timeout(&mut self, command_timeout: Duration)
pub fn set_command_timeout(&mut self, command_timeout: Duration)
Changes the command timeout of sweep_timeouts. A connector sets it a
little above its request timeout, so that an order is not given up while its call is still running.
Sourcepub fn with_retention(self, window: Duration, max_finished: usize) -> Self
pub fn with_retention(self, window: Duration, max_finished: usize) -> Self
Sets the memory window of finished orders: they are forgotten after window, or earlier when
more than max_finished are kept (the oldest go first). See the type docs for what this means for
the “a cid is placed once” guarantee.
§Examples
use std::time::{Duration, Instant};
use brokapi_core::ClientOrderId;
use brokapi_runtime::OrderRuntime;
use brokapi_sansio::OrderSignal;
let mut orders = OrderRuntime::new().with_retention(Duration::from_secs(60), 1000);
let t0 = Instant::now();
let cid = ClientOrderId::new_v4();
assert!(orders.try_place_once(cid));
orders.register_place(cid, t0);
orders.apply_signal(cid, OrderSignal::Rejected { reason: "no".into() }, t0);
// Inside the window the cid is still refused ...
orders.prune(t0 + Duration::from_secs(30));
assert!(!orders.try_place_once(cid));
// ... after it the order is forgotten.
orders.prune(t0 + Duration::from_secs(61));
assert!(orders.state(cid).is_none());
assert!(orders.try_place_once(cid));Sourcepub fn prune(&mut self, now: Instant)
pub fn prune(&mut self, now: Instant)
Forgets the finished orders that are older than the window, or beyond the count limit, as of now.
Called by apply_signal itself, so a caller rarely needs it.
Sourcepub fn tracked(&self) -> usize
pub fn tracked(&self) -> usize
How many orders are tracked now, in any state. For tests and metrics.
Sourcepub fn remembered_places(&self) -> usize
pub fn remembered_places(&self) -> usize
How many cids are remembered as used for a place. For tests and metrics.
Sourcepub fn in_flight_count(&self) -> usize
pub fn in_flight_count(&self) -> usize
How many orders are InFlight now: sent, with no answer yet.
Sourcepub fn register_place(&mut self, cid: ClientOrderId, now: Instant)
pub fn register_place(&mut self, cid: ClientOrderId, now: Instant)
Starts tracking a place command that is being sent at now, in state InFlight.
Records the first (and only allowed) attempt for cid. Registering a cid again restarts its
state machine from InFlight.
Sourcepub fn register_cancel(&mut self, cid: ClientOrderId, now: Instant)
pub fn register_cancel(&mut self, cid: ClientOrderId, now: Instant)
Starts tracking a cid known only from a cancel, if it is not tracked yet.
An order placed by an earlier run is not in memory. A cid that is already tracked keeps its
state. A new one starts as InFlight, and does not count as a place attempt.
Sourcepub fn may_send_place(&mut self, cid: ClientOrderId) -> bool
pub fn may_send_place(&mut self, cid: ClientOrderId) -> bool
Returns true and records the attempt when cid has never been used for a place, false
when it has.
Why: the library must not retry a place on its own. Compare
try_place_once, which does the same.
Sourcepub fn try_place_once(&mut self, cid: ClientOrderId) -> bool
pub fn try_place_once(&mut self, cid: ClientOrderId) -> bool
Returns true only on the first place attempt for this cid, and records that attempt.
Call it before sending. The attempt is recorded even when the send then fails locally (for
example a rate-limit refusal), so the same cid is never used twice. To try again, make a new
cid. After register_place the answer is always false. The record is
kept for the memory window of the type, see there.
§Examples
use brokapi_core::ClientOrderId;
use brokapi_runtime::OrderRuntime;
let mut orders = OrderRuntime::new();
let cid = ClientOrderId::new_v4();
assert!(orders.try_place_once(cid));
assert!(!orders.try_place_once(cid));
assert!(orders.try_place_once(ClientOrderId::new_v4()));Sourcepub fn apply_signal(
&mut self,
cid: ClientOrderId,
signal: OrderSignal,
now: Instant,
) -> Option<OrderState>
pub fn apply_signal( &mut self, cid: ClientOrderId, signal: OrderSignal, now: Instant, ) -> Option<OrderState>
Feeds a signal (ack, fill, cancel, reject, timeout) into the order’s state machine.
Returns the new state when the state changed. Returns None when nothing changed, when
the cid is not tracked, and when the signal is not valid in the current state (for example
a cancel after Filled). An invalid signal is logged at warn level. A caller that needs to tell
these apart uses try_apply_signal.
Sourcepub fn try_apply_signal(
&mut self,
cid: ClientOrderId,
signal: OrderSignal,
now: Instant,
) -> Result<Option<OrderState>, FsmError>
pub fn try_apply_signal( &mut self, cid: ClientOrderId, signal: OrderSignal, now: Instant, ) -> Result<Option<OrderState>, FsmError>
Like apply_signal but tells the reason when the signal is not applied.
§Errors
[FsmError::UnknownOrder] for a cid that is not tracked, [FsmError::InvalidTransition] for a
signal that contradicts the state (a cancel of a filled order). The state is left as it was.
§Examples
use std::time::Instant;
use brokapi_core::ClientOrderId;
use brokapi_runtime::OrderRuntime;
use brokapi_sansio::{FsmError, OrderSignal};
let mut orders = OrderRuntime::new();
let t0 = Instant::now();
let cid = ClientOrderId::new_v4();
assert!(matches!(
orders.try_apply_signal(cid, OrderSignal::Filled, t0),
Err(FsmError::UnknownOrder(_))
));
// A fill that is faster than the answer to the place command is taken.
orders.register_place(cid, t0);
assert!(orders.try_apply_signal(cid, OrderSignal::Filled, t0).unwrap().is_some());
// A cancel of a filled order contradicts it.
assert!(orders.try_apply_signal(cid, OrderSignal::Cancelled, t0).is_err());Sourcepub fn sweep_timeouts(
&mut self,
now: Instant,
) -> Vec<(ClientOrderId, OrderState)>
pub fn sweep_timeouts( &mut self, now: Instant, ) -> Vec<(ClientOrderId, OrderState)>
Moves every InFlight order older than the command timeout to Unknown.
Returns the orders that changed, with their new state. It does not cancel or retry anything: the exchange order may well exist.
Sourcepub fn state(&self, cid: ClientOrderId) -> Option<&OrderState>
pub fn state(&self, cid: ClientOrderId) -> Option<&OrderState>
The current state of a tracked order, or None for an unknown cid.
Sourcepub fn abandon_inflight(&mut self, now: Instant) -> Vec<ClientOrderId> ⓘ
pub fn abandon_inflight(&mut self, now: Instant) -> Vec<ClientOrderId> ⓘ
Moves every InFlight order to Unknown now, whatever its age. Used at shutdown.
Returns the cids that changed. Orders at the broker are untouched, only the local wait for an answer is given up.