#![doc = include_str!("../README.md")]
use std::error::Error;
use std::fmt;
use std::hash::{Hash, Hasher};
pub const EXECUTION_TEXT_CAP: usize = 128;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct FixedAscii<const N: usize> {
len: u8,
bytes: [u8; N],
}
impl<const N: usize> FixedAscii<N> {
pub const fn empty() -> Self {
Self {
len: 0,
bytes: [0; N],
}
}
pub fn new(value: &str) -> Result<Self, ExecutionCoreError> {
if value.len() > N {
return Err(ExecutionCoreError::IdentifierTooLong {
capacity: N,
actual: value.len(),
});
}
if !value.is_ascii() {
return Err(ExecutionCoreError::NonAsciiIdentifier);
}
let mut bytes = [0; N];
bytes[..value.len()].copy_from_slice(value.as_bytes());
Ok(Self {
len: value.len() as u8,
bytes,
})
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.bytes[..self.len as usize])
.expect("FixedAscii stores only validated ASCII")
}
pub const fn capacity(&self) -> usize {
N
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
impl<const N: usize> Default for FixedAscii<N> {
fn default() -> Self {
Self::empty()
}
}
impl<const N: usize> fmt::Debug for FixedAscii<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("FixedAscii").field(&self.as_str()).finish()
}
}
impl<const N: usize> fmt::Display for FixedAscii<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<const N: usize> PartialEq for FixedAscii<N> {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl<const N: usize> Eq for FixedAscii<N> {}
impl<const N: usize> Hash for FixedAscii<N> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
pub type ClientOrderId = FixedAscii<40>;
pub type VenueOrderId = FixedAscii<48>;
pub type ExecutionId = FixedAscii<48>;
pub type AccountId = FixedAscii<32>;
pub type RouteId = FixedAscii<32>;
pub type StrategyId = FixedAscii<32>;
pub type VenueId = FixedAscii<16>;
pub type InstrumentId = FixedAscii<32>;
pub type ExecutionText = FixedAscii<EXECUTION_TEXT_CAP>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionCoreError {
IdentifierTooLong {
capacity: usize,
actual: usize,
},
NonAsciiIdentifier,
InvalidQuantity,
InvalidPrice,
InvalidTransition,
}
impl fmt::Display for ExecutionCoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IdentifierTooLong { capacity, actual } => {
write!(f, "identifier length {actual} exceeds capacity {capacity}")
}
Self::NonAsciiIdentifier => write!(f, "identifier must be ASCII"),
Self::InvalidQuantity => write!(f, "quantity must be positive"),
Self::InvalidPrice => write!(f, "price must be positive"),
Self::InvalidTransition => write!(f, "invalid order state transition"),
}
}
}
impl Error for ExecutionCoreError {}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ExecutionSymbol {
pub venue: VenueId,
pub instrument: InstrumentId,
}
impl ExecutionSymbol {
pub fn new(venue: &str, instrument: &str) -> Result<Self, ExecutionCoreError> {
Ok(Self {
venue: VenueId::new(venue)?,
instrument: InstrumentId::new(instrument)?,
})
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct OrderQty(pub i64);
impl OrderQty {
pub fn new(value: i64) -> Result<Self, ExecutionCoreError> {
if value <= 0 {
return Err(ExecutionCoreError::InvalidQuantity);
}
Ok(Self(value))
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct OrderPrice(pub i64);
impl OrderPrice {
pub fn new(value: i64) -> Result<Self, ExecutionCoreError> {
if value <= 0 {
return Err(ExecutionCoreError::InvalidPrice);
}
Ok(Self(value))
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderSide {
Buy = 1,
Sell = 2,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderType {
Market = 1,
Limit = 2,
Stop = 3,
StopLimit = 4,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeInForce {
Day = 1,
Gtc = 2,
Ioc = 3,
Fok = 4,
Gtd = 5,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderStatus {
PendingNew = 1,
New = 2,
PartiallyFilled = 3,
Filled = 4,
PendingCancel = 5,
Cancelled = 6,
PendingReplace = 7,
Replaced = 8,
Rejected = 9,
Expired = 10,
Suspended = 11,
Unknown = 12,
}
impl OrderStatus {
pub const fn is_terminal(self) -> bool {
matches!(
self,
Self::Filled | Self::Cancelled | Self::Rejected | Self::Expired
)
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExecutionType {
Ack = 1,
Reject = 2,
Trade = 3,
CancelPending = 4,
CancelAck = 5,
CancelReject = 6,
ReplacePending = 7,
ReplaceAck = 8,
ReplaceReject = 9,
Expire = 10,
Status = 11,
Restated = 12,
AdapterDegraded = 13,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderRequest {
pub client_order_id: ClientOrderId,
pub account_id: AccountId,
pub route_id: RouteId,
pub strategy_id: StrategyId,
pub symbol: ExecutionSymbol,
pub side: OrderSide,
pub order_type: OrderType,
pub time_in_force: TimeInForce,
pub quantity: OrderQty,
pub limit_price: OrderPrice,
pub stop_price: OrderPrice,
pub ts_exchange_ns: u64,
pub ts_recv_ns: u64,
}
impl OrderRequest {
pub fn validate(&self) -> Result<(), ExecutionCoreError> {
if self.quantity.0 <= 0 {
return Err(ExecutionCoreError::InvalidQuantity);
}
match self.order_type {
OrderType::Limit | OrderType::StopLimit if self.limit_price.0 <= 0 => {
Err(ExecutionCoreError::InvalidPrice)
}
OrderType::Stop | OrderType::StopLimit if self.stop_price.0 <= 0 => {
Err(ExecutionCoreError::InvalidPrice)
}
_ => Ok(()),
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CancelRequest {
pub client_order_id: ClientOrderId,
pub orig_client_order_id: ClientOrderId,
pub venue_order_id: VenueOrderId,
pub account_id: AccountId,
pub route_id: RouteId,
pub symbol: ExecutionSymbol,
pub ts_recv_ns: u64,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmendRequest {
pub client_order_id: ClientOrderId,
pub orig_client_order_id: ClientOrderId,
pub venue_order_id: VenueOrderId,
pub account_id: AccountId,
pub route_id: RouteId,
pub symbol: ExecutionSymbol,
pub quantity: OrderQty,
pub limit_price: OrderPrice,
pub ts_recv_ns: u64,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutionEvent {
pub exec_type: ExecutionType,
pub order_status: OrderStatus,
pub client_order_id: ClientOrderId,
pub orig_client_order_id: ClientOrderId,
pub venue_order_id: VenueOrderId,
pub execution_id: ExecutionId,
pub account_id: AccountId,
pub route_id: RouteId,
pub symbol: ExecutionSymbol,
pub last_qty: OrderQty,
pub last_price: OrderPrice,
pub cumulative_qty: OrderQty,
pub leaves_qty: OrderQty,
pub average_price: OrderPrice,
pub ts_exchange_ns: u64,
pub ts_recv_ns: u64,
pub reason: RiskRejectReason,
pub text: ExecutionText,
}
impl ExecutionEvent {
pub fn accepted(req: &OrderRequest, venue_order_id: VenueOrderId) -> Self {
Self {
exec_type: ExecutionType::Ack,
order_status: OrderStatus::New,
client_order_id: req.client_order_id,
orig_client_order_id: ClientOrderId::empty(),
venue_order_id,
execution_id: ExecutionId::empty(),
account_id: req.account_id,
route_id: req.route_id,
symbol: req.symbol,
last_qty: OrderQty(0),
last_price: OrderPrice(0),
cumulative_qty: OrderQty(0),
leaves_qty: req.quantity,
average_price: OrderPrice(0),
ts_exchange_ns: req.ts_exchange_ns,
ts_recv_ns: req.ts_recv_ns,
reason: RiskRejectReason::None,
text: ExecutionText::empty(),
}
}
pub fn rejected(req: &OrderRequest, reason: RiskRejectReason, text: ExecutionText) -> Self {
Self {
exec_type: ExecutionType::Reject,
order_status: OrderStatus::Rejected,
client_order_id: req.client_order_id,
orig_client_order_id: ClientOrderId::empty(),
venue_order_id: VenueOrderId::empty(),
execution_id: ExecutionId::empty(),
account_id: req.account_id,
route_id: req.route_id,
symbol: req.symbol,
last_qty: OrderQty(0),
last_price: OrderPrice(0),
cumulative_qty: OrderQty(0),
leaves_qty: req.quantity,
average_price: OrderPrice(0),
ts_exchange_ns: req.ts_exchange_ns,
ts_recv_ns: req.ts_recv_ns,
reason,
text,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderState {
pub client_order_id: ClientOrderId,
pub last_accepted_client_order_id: ClientOrderId,
pub venue_order_id: VenueOrderId,
pub account_id: AccountId,
pub route_id: RouteId,
pub symbol: ExecutionSymbol,
pub side: OrderSide,
pub status: OrderStatus,
pub order_qty: OrderQty,
pub cumulative_qty: OrderQty,
pub leaves_qty: OrderQty,
pub average_price: OrderPrice,
pub updated_ns: u64,
}
impl OrderState {
pub fn pending_new(req: &OrderRequest) -> Self {
Self {
client_order_id: req.client_order_id,
last_accepted_client_order_id: req.client_order_id,
venue_order_id: VenueOrderId::empty(),
account_id: req.account_id,
route_id: req.route_id,
symbol: req.symbol,
side: req.side,
status: OrderStatus::PendingNew,
order_qty: req.quantity,
cumulative_qty: OrderQty(0),
leaves_qty: req.quantity,
average_price: OrderPrice(0),
updated_ns: req.ts_recv_ns,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrderStateMachine {
state: OrderState,
}
impl OrderStateMachine {
pub fn new(req: &OrderRequest) -> Self {
Self {
state: OrderState::pending_new(req),
}
}
pub const fn state(&self) -> &OrderState {
&self.state
}
pub fn apply(&mut self, event: &ExecutionEvent) -> Result<(), ExecutionCoreError> {
if self.state.status.is_terminal() && event.exec_type != ExecutionType::Status {
return Err(ExecutionCoreError::InvalidTransition);
}
match event.exec_type {
ExecutionType::Ack => self.apply_ack(event),
ExecutionType::Reject => self.apply_terminal(event, OrderStatus::Rejected),
ExecutionType::Trade => self.apply_trade(event),
ExecutionType::CancelPending => self.apply_pending(event, OrderStatus::PendingCancel),
ExecutionType::CancelAck => self.apply_terminal(event, OrderStatus::Cancelled),
ExecutionType::CancelReject => self.apply_status(event),
ExecutionType::ReplacePending => self.apply_pending(event, OrderStatus::PendingReplace),
ExecutionType::ReplaceAck => self.apply_replace(event),
ExecutionType::ReplaceReject => self.apply_status(event),
ExecutionType::Expire => self.apply_terminal(event, OrderStatus::Expired),
ExecutionType::Status | ExecutionType::Restated | ExecutionType::AdapterDegraded => {
self.apply_status(event)
}
}
}
fn apply_ack(&mut self, event: &ExecutionEvent) -> Result<(), ExecutionCoreError> {
if self.state.status != OrderStatus::PendingNew {
return Err(ExecutionCoreError::InvalidTransition);
}
self.state.status = OrderStatus::New;
self.state.venue_order_id = event.venue_order_id;
self.state.leaves_qty = event.leaves_qty;
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
fn apply_trade(&mut self, event: &ExecutionEvent) -> Result<(), ExecutionCoreError> {
if event.cumulative_qty.0 > self.state.order_qty.0 {
return Err(ExecutionCoreError::InvalidTransition);
}
self.state.cumulative_qty = event.cumulative_qty;
self.state.leaves_qty = event.leaves_qty;
self.state.average_price = event.average_price;
self.state.status = if event.leaves_qty.0 == 0 {
OrderStatus::Filled
} else {
OrderStatus::PartiallyFilled
};
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
fn apply_pending(
&mut self,
event: &ExecutionEvent,
status: OrderStatus,
) -> Result<(), ExecutionCoreError> {
if matches!(
self.state.status,
OrderStatus::PendingNew | OrderStatus::PendingCancel | OrderStatus::PendingReplace
) {
return Err(ExecutionCoreError::InvalidTransition);
}
self.state.status = status;
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
fn apply_replace(&mut self, event: &ExecutionEvent) -> Result<(), ExecutionCoreError> {
if self.state.status != OrderStatus::PendingReplace {
return Err(ExecutionCoreError::InvalidTransition);
}
self.state.client_order_id = event.client_order_id;
self.state.last_accepted_client_order_id = event.client_order_id;
self.state.status = OrderStatus::Replaced;
self.state.order_qty = OrderQty(event.cumulative_qty.0 + event.leaves_qty.0);
self.state.cumulative_qty = event.cumulative_qty;
self.state.leaves_qty = event.leaves_qty;
self.state.average_price = event.average_price;
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
fn apply_terminal(
&mut self,
event: &ExecutionEvent,
status: OrderStatus,
) -> Result<(), ExecutionCoreError> {
self.state.status = status;
self.state.cumulative_qty = event.cumulative_qty;
self.state.leaves_qty = event.leaves_qty;
self.state.average_price = event.average_price;
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
fn apply_status(&mut self, event: &ExecutionEvent) -> Result<(), ExecutionCoreError> {
if event.order_status == OrderStatus::Unknown {
self.state.status = OrderStatus::Unknown;
} else if event.order_status == OrderStatus::Suspended {
self.state.status = OrderStatus::Suspended;
} else if matches!(
event.exec_type,
ExecutionType::CancelReject | ExecutionType::ReplaceReject
) {
self.state.status = event.order_status;
}
self.state.updated_ns = event.ts_recv_ns;
Ok(())
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RiskRejectReason {
None = 0,
KillSwitch = 1,
AccountDisabled = 2,
RouteDisabled = 3,
SymbolDisabled = 4,
MaxOrderQty = 5,
MaxOrderNotional = 6,
MaxOpenOrders = 7,
MaxOpenNotional = 8,
PriceBand = 9,
DuplicateClientOrderId = 10,
UnsupportedOrderType = 11,
UnsupportedTimeInForce = 12,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RiskDecision {
pub allowed: bool,
pub reason: RiskRejectReason,
pub text: ExecutionText,
}
impl RiskDecision {
pub const fn allow() -> Self {
Self {
allowed: true,
reason: RiskRejectReason::None,
text: ExecutionText::empty(),
}
}
pub fn reject(reason: RiskRejectReason, text: ExecutionText) -> Self {
Self {
allowed: false,
reason,
text,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RiskLimits {
pub kill_switch: bool,
pub max_order_qty: i64,
pub max_order_notional: i128,
pub max_open_orders: u32,
pub max_open_notional: i128,
pub price_band_ticks: i64,
}
impl Default for RiskLimits {
fn default() -> Self {
Self {
kill_switch: true,
max_order_qty: 0,
max_order_notional: 0,
max_open_orders: 0,
max_open_notional: 0,
price_band_ticks: 0,
}
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RiskContext {
pub open_orders: u32,
pub open_notional: i128,
pub reference_price: OrderPrice,
pub duplicate_client_order_id: bool,
pub account_enabled: bool,
pub route_enabled: bool,
pub symbol_enabled: bool,
pub order_type_supported: bool,
pub tif_supported: bool,
}
impl Default for RiskContext {
fn default() -> Self {
Self {
open_orders: 0,
open_notional: 0,
reference_price: OrderPrice(0),
duplicate_client_order_id: false,
account_enabled: false,
route_enabled: false,
symbol_enabled: false,
order_type_supported: false,
tif_supported: false,
}
}
}
pub trait RiskCheck: Send + Sync {
fn check_new(&self, req: &OrderRequest, ctx: &RiskContext) -> RiskDecision;
fn check_amend(&self, req: &AmendRequest, ctx: &RiskContext) -> RiskDecision;
fn check_cancel(&self, req: &CancelRequest, ctx: &RiskContext) -> RiskDecision;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BasicRiskGate {
limits: RiskLimits,
}
impl BasicRiskGate {
pub const fn new(limits: RiskLimits) -> Self {
Self { limits }
}
fn check_common(&self, ctx: &RiskContext) -> RiskDecision {
if self.limits.kill_switch {
return reject(RiskRejectReason::KillSwitch, "kill switch active");
}
if !ctx.account_enabled {
return reject(RiskRejectReason::AccountDisabled, "account disabled");
}
if !ctx.route_enabled {
return reject(RiskRejectReason::RouteDisabled, "route disabled");
}
if !ctx.symbol_enabled {
return reject(RiskRejectReason::SymbolDisabled, "symbol disabled");
}
if ctx.duplicate_client_order_id {
return reject(
RiskRejectReason::DuplicateClientOrderId,
"duplicate client order id",
);
}
if !ctx.order_type_supported {
return reject(
RiskRejectReason::UnsupportedOrderType,
"unsupported order type",
);
}
if !ctx.tif_supported {
return reject(
RiskRejectReason::UnsupportedTimeInForce,
"unsupported time in force",
);
}
if self.limits.max_open_orders > 0 && ctx.open_orders >= self.limits.max_open_orders {
return reject(RiskRejectReason::MaxOpenOrders, "max open orders exceeded");
}
if self.limits.max_open_notional > 0 && ctx.open_notional >= self.limits.max_open_notional {
return reject(
RiskRejectReason::MaxOpenNotional,
"max open notional exceeded",
);
}
RiskDecision::allow()
}
fn check_size_price(
&self,
qty: OrderQty,
price: OrderPrice,
ctx: &RiskContext,
) -> RiskDecision {
if self.limits.max_order_qty > 0 && qty.0 > self.limits.max_order_qty {
return reject(RiskRejectReason::MaxOrderQty, "max order quantity exceeded");
}
if self.limits.max_order_notional > 0 {
let notional = i128::from(qty.0).saturating_mul(i128::from(price.0));
if notional > self.limits.max_order_notional {
return reject(
RiskRejectReason::MaxOrderNotional,
"max order notional exceeded",
);
}
}
if self.limits.price_band_ticks > 0 && ctx.reference_price.0 > 0 && price.0 > 0 {
let distance = price.0.saturating_sub(ctx.reference_price.0).abs();
if distance > self.limits.price_band_ticks {
return reject(RiskRejectReason::PriceBand, "price outside risk band");
}
}
RiskDecision::allow()
}
}
impl RiskCheck for BasicRiskGate {
fn check_new(&self, req: &OrderRequest, ctx: &RiskContext) -> RiskDecision {
let common = self.check_common(ctx);
if !common.allowed {
return common;
}
self.check_size_price(req.quantity, req.limit_price, ctx)
}
fn check_amend(&self, req: &AmendRequest, ctx: &RiskContext) -> RiskDecision {
let common = self.check_common(ctx);
if !common.allowed {
return common;
}
self.check_size_price(req.quantity, req.limit_price, ctx)
}
fn check_cancel(&self, _req: &CancelRequest, ctx: &RiskContext) -> RiskDecision {
if self.limits.kill_switch {
return reject(RiskRejectReason::KillSwitch, "kill switch active");
}
if !ctx.account_enabled {
return reject(RiskRejectReason::AccountDisabled, "account disabled");
}
if !ctx.route_enabled {
return reject(RiskRejectReason::RouteDisabled, "route disabled");
}
RiskDecision::allow()
}
}
fn reject(reason: RiskRejectReason, text: &str) -> RiskDecision {
let text = ExecutionText::new(text).unwrap_or_else(|_| ExecutionText::empty());
RiskDecision::reject(reason, text)
}
#[cfg(test)]
mod tests {
use super::*;
fn id<const N: usize>(value: &str) -> FixedAscii<N> {
FixedAscii::new(value).unwrap()
}
fn order_request() -> OrderRequest {
OrderRequest {
client_order_id: id("C1"),
account_id: id("A1"),
route_id: id("R1"),
strategy_id: id("S1"),
symbol: ExecutionSymbol::new("CME", "ESM6").unwrap(),
side: OrderSide::Buy,
order_type: OrderType::Limit,
time_in_force: TimeInForce::Day,
quantity: OrderQty(10),
limit_price: OrderPrice(5000),
stop_price: OrderPrice(0),
ts_exchange_ns: 1,
ts_recv_ns: 2,
}
}
fn live_ctx() -> RiskContext {
RiskContext {
open_orders: 0,
open_notional: 0,
reference_price: OrderPrice(5000),
duplicate_client_order_id: false,
account_enabled: true,
route_enabled: true,
symbol_enabled: true,
order_type_supported: true,
tif_supported: true,
}
}
#[test]
fn fixed_ascii_rejects_invalid_input() {
assert_eq!(
ClientOrderId::new("abcdefghijklmnopqrstuvwxyz1234567890ABCDE").unwrap_err(),
ExecutionCoreError::IdentifierTooLong {
capacity: 40,
actual: 41
}
);
assert_eq!(
ClientOrderId::new("ordé").unwrap_err(),
ExecutionCoreError::NonAsciiIdentifier
);
}
#[test]
fn order_validation_requires_limit_price() {
let mut req = order_request();
req.limit_price = OrderPrice(0);
assert_eq!(req.validate(), Err(ExecutionCoreError::InvalidPrice));
}
#[test]
fn state_machine_accepts_and_fills_order() {
let req = order_request();
let mut sm = OrderStateMachine::new(&req);
let ack = ExecutionEvent::accepted(&req, id("V1"));
sm.apply(&ack).unwrap();
assert_eq!(sm.state().status, OrderStatus::New);
let mut fill = ack;
fill.exec_type = ExecutionType::Trade;
fill.order_status = OrderStatus::Filled;
fill.execution_id = id("E1");
fill.last_qty = OrderQty(10);
fill.last_price = OrderPrice(5001);
fill.cumulative_qty = OrderQty(10);
fill.leaves_qty = OrderQty(0);
fill.average_price = OrderPrice(5001);
fill.ts_recv_ns = 3;
sm.apply(&fill).unwrap();
assert_eq!(sm.state().status, OrderStatus::Filled);
assert_eq!(sm.state().cumulative_qty, OrderQty(10));
assert!(sm.apply(&fill).is_err());
}
#[test]
fn state_machine_handles_cancel_reject_as_status() {
let req = order_request();
let mut sm = OrderStateMachine::new(&req);
sm.apply(&ExecutionEvent::accepted(&req, id("V1"))).unwrap();
let mut pending_cancel = ExecutionEvent::accepted(&req, id("V1"));
pending_cancel.exec_type = ExecutionType::CancelPending;
pending_cancel.order_status = OrderStatus::PendingCancel;
pending_cancel.ts_recv_ns = 4;
sm.apply(&pending_cancel).unwrap();
let mut reject = pending_cancel;
reject.exec_type = ExecutionType::CancelReject;
reject.order_status = OrderStatus::New;
reject.ts_recv_ns = 5;
sm.apply(&reject).unwrap();
assert_eq!(sm.state().status, OrderStatus::New);
}
#[test]
fn risk_gate_denies_by_default() {
let req = order_request();
let gate = BasicRiskGate::new(RiskLimits::default());
let decision = gate.check_new(&req, &RiskContext::default());
assert!(!decision.allowed);
assert_eq!(decision.reason, RiskRejectReason::KillSwitch);
}
#[test]
fn risk_gate_allows_configured_order() {
let req = order_request();
let gate = BasicRiskGate::new(RiskLimits {
kill_switch: false,
max_order_qty: 100,
max_order_notional: 1_000_000,
max_open_orders: 10,
max_open_notional: 10_000_000,
price_band_ticks: 10,
});
let decision = gate.check_new(&req, &live_ctx());
assert!(decision.allowed);
}
#[test]
fn risk_gate_rejects_price_band() {
let mut req = order_request();
req.limit_price = OrderPrice(5020);
let gate = BasicRiskGate::new(RiskLimits {
kill_switch: false,
max_order_qty: 100,
max_order_notional: 1_000_000,
max_open_orders: 10,
max_open_notional: 10_000_000,
price_band_ticks: 10,
});
let decision = gate.check_new(&req, &live_ctx());
assert!(!decision.allowed);
assert_eq!(decision.reason, RiskRejectReason::PriceBand);
}
}